Вопрос

Как проверить, работает ли фоновая служба?

Мне нужна активность Android, которая переключает состояние службы: она позволяет мне включать ее, если она выключена, и выключать, если она включена.

Это было полезно?

Решение

У меня была такая же проблема не так давно.Поскольку мой сервис был локальным, я просто использовал статическое поле в классе сервиса для переключения состояния, как описано hackbod. здесь

РЕДАКТИРОВАТЬ (для записи):

Вот решение, предложенное hackbod:

Если ваш клиент и код сервера является частью того же. APK, и вы привязываете к сервису с конкретным намерением (тот, который определяет точный класс обслуживания), то вы можете просто установить глобальную переменную, когда она работает, это Ваш клиент может проверить.

У нас намеренно нет API, чтобы проверить, работает ли услуга, потому что, почти без обязательств, когда вы хотите сделать что -то подобное, вы получаете условия гонки в своем коде.

Другие советы

Я использую следующее изнутри действия:

private boolean isMyServiceRunning(Class<?> serviceClass) {
    ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
        if (serviceClass.getName().equals(service.service.getClassName())) {
            return true;
        }
    }
    return false;
}

И я называю это, используя:

isMyServiceRunning(MyService.class)

Это работает надежно, поскольку основано на информации о запущенных службах, предоставляемых операционной системой Android через ActivityManager#getRunningServices.

Все подходы, использующие события onDestroy или onSometing, привязки или статические переменные, не будут работать надежно, потому что, как разработчик, вы никогда не знаете, когда Android решит завершить ваш процесс или какие из упомянутых обратных вызовов будут вызваны или нет.Обратите внимание на столбец «убиваемые» в таблица событий жизненного цикла в документации Android.

Понятно!

Ты ДОЛЖЕН вызов startService() чтобы ваша услуга была правильно зарегистрирована и прошла BIND_AUTO_CREATE будет недостаточно.

Intent bindIntent = new Intent(this,ServiceTask.class);
startService(bindIntent);
bindService(bindIntent,mConnection,0);

А теперь класс ServiceTools:

public class ServiceTools {
    private static String LOG_TAG = ServiceTools.class.getName();

    public static boolean isServiceRunning(String serviceClassName){
        final ActivityManager activityManager = (ActivityManager)Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
        final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

        for (RunningServiceInfo runningServiceInfo : services) {
            if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
                return true;
            }
        }
        return false;
     }
}

Небольшое дополнение:

Моя цель — узнать, работает ли служба, не запуская ее, если она не запущена.

Вызов метода связывания или вызова намерения, которое может быть перехвачено службой, не является хорошей идеей, так как служба запустится, если она не запущена.

Итак, как предположил чудо2к, лучше всего иметь статическое поле в классе службы, чтобы знать, запущена служба или нет.

Чтобы сделать его еще чище, я предлагаю преобразовать сервис в синглтон с очень-очень ленивой выборкой:то есть вообще не существует создания экземпляров синглтон экземпляр через статические методы.Статический метод getInstance вашего сервиса/синглтона просто возвращает экземпляр синглтона, если он был создан.Но на самом деле он не запускает и не создает экземпляр самого синглтона.Служба запускается только обычными методами запуска службы.

Тогда было бы еще проще изменить шаблон проектирования Singleton, переименовав запутанный метод getInstance во что-то вроде isInstanceCreated() : boolean метод.

Код будет выглядеть так:

public class MyService extends Service
{
   private static MyService instance = null;

   public static boolean isInstanceCreated() {
      return instance != null;
   }//met

   @Override
   public void onCreate()
   {
      instance = this;
      ....
   }//met

   @Override
   public void onDestroy()
   {
      instance = null;
      ...
   }//met
}//class

Это решение элегантно, но оно актуально только в том случае, если у вас есть доступ к классу службы и только для классов, находящихся вне приложения/пакета службы.Если ваши классы находятся за пределами приложения/пакета службы, вы можете запросить ActivityManager с ограничениями, подчеркнутыми Питером-Яном Ван Робейсом.

Вы можете использовать это (я еще не пробовал, но надеюсь, что это сработает):

if(startService(someIntent) != null) {
    Toast.makeText(getBaseContext(), "Service is already running", Toast.LENGTH_SHORT).show();
}
else {
    Toast.makeText(getBaseContext(), "There is no service running, starting service..", Toast.LENGTH_SHORT).show();
}

Метод startService возвращает объект ComponentName, если уже запущена служба.Если нет, будет возвращено значение null.

Видеть общедоступное абстрактное имя компонента startService (служба намерений).

Я думаю, это не похоже на проверку, потому что она запускает службу, поэтому вы можете добавить stopService(someIntent); под кодом.

    public boolean checkServiceRunning(){
         ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) 
                {
                    if ("com.example.yourpackagename.YourServiceName"
                            .equals(service.service.getClassName())) 
                    {
                        return true;
                    }
                }
             return false;
    }

Я немного изменил одно из решений, представленных выше, но передал класс вместо общего имени строки, чтобы обязательно сравнивать строки, полученные из одного и того же метода. class.getName()

public class ServiceTools {
    private static String LOG_TAG = ServiceTools.class.getName();

    public static boolean isServiceRunning(Context context,Class<?> serviceClass){
        final ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
        final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

        for (RunningServiceInfo runningServiceInfo : services) {
            Log.d(Constants.TAG, String.format("Service:%s", runningServiceInfo.service.getClassName()));
            if (runningServiceInfo.service.getClassName().equals(serviceClass.getName())){
                return true;
            }
        }
        return false;
    }
}

а потом

Boolean isServiceRunning = ServiceTools.isServiceRunning(
                    MainActivity.this.getApplicationContext(),
                    BackgroundIntentService.class);

Выдержка из Андроид документы:

Нравиться sendBroadcast (Намерение), но если есть какие -либо приемники для намерения, эта функция будет блокировать и немедленно отправлять их перед возвращением.

Думайте об этом хаке как о «пинговании» Service поскольку мы можем транслировать синхронно, мы можем транслировать и получать результат — синхронно — в потоке пользовательского интерфейса.

Service

@Override
public void onCreate() {
   LocalBroadcastManager
     .getInstance(this)
     .registerReceiver(new ServiceEchoReceiver(), new IntentFilter("ping"));
}

private class ServiceEchoReceiver extends BroadcastReceiver {
    public void onReceive (Context context, Intent intent) {
      LocalBroadcastManager
         .getInstance(this)
         .sendBroadcastSync(new Intent("pong"));
    }
}

Activity

    bool serviceRunning = false;

    protected void onCreate (Bundle savedInstanceState){
        LocalBroadcastManager.getInstance(this).registerReceiver(pong, new IntentFilter("pong"));
        LocalBroadcastManager.getInstance(this).sendBroadcastSync(new Intent("ping"));
        if(!serviceRunning){
           //run the service
        }
    }

    private BroadcastReceiver pong = new BroadcastReceiver(){
        public void onReceive (Context context, Intent intent) {
          serviceRunning = true;   
        }
    }

Я просто хочу добавить примечание к ответу @Snicolas.Следующие шаги можно использовать для проверки остановки службы с вызовом или без него. onDestroy().

  1. onDestroy() называется:Перейдите в «Настройки» -> «Приложение» -> «Запуск служб» -> «Выбрать и остановить службу».

  2. onDestroy() не вызывается:Перейдите в «Настройки» -> «Приложение» -> «Управление приложениями» -> «Выберите и принудительно остановите» приложение, в котором работает ваша служба.Однако, поскольку ваше приложение здесь остановлено, экземпляры службы также будут остановлены.

Наконец, я хотел бы отметить, что упомянутый там подход с использованием статической переменной в одноэлементном классе работает для меня.

onDestroy не всегда вызывается в сервисе, так что это бесполезно!

Например:Просто запустите приложение еще раз с одним изменением по сравнению с Eclipse.Приложение принудительно закрывается с помощью SIG:9.

Правильный способ проверить, работает ли служба, — это просто спросить ее.Внедрите в свою службу BroadcastReceiver, который будет отвечать на запросы вашей активности.Зарегистрируйте BroadcastReceiver при запуске службы и отмените его регистрацию, когда служба будет уничтожена.Из вашей активности (или любого компонента) отправьте местное вещание намерение службы, и если она отвечает, вы знаете, что она работает.Обратите внимание на тонкую разницу между ACTION_PING и ACTION_PONG в приведенном ниже коде.

public class PingableService extends Service
{
    public static final String ACTION_PING = PingableService.class.getName() + ".PING";
    public static final String ACTION_PONG = PingableService.class.getName() + ".PONG";

    public int onStartCommand (Intent intent, int flags, int startId)
    {
        LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(ACTION_PING));
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy ()
    {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
        super.onDestroy();
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver()
    {
        @Override
        public void onReceive (Context context, Intent intent)
        {
            if (intent.getAction().equals(ACTION_PING))
            {
                LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getApplicationContext());
                manager.sendBroadcast(new Intent(ACTION_PONG));
            }
        }
    };
}


public class MyActivity extends Activity
{
    private boolean isSvcRunning = false;

    @Override
    protected void onStart()
    {
        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getApplicationContext());
        manager.registerReceiver(mReceiver, new IntentFilter(PingableService.ACTION_PONG));
        // the service will respond to this broadcast only if it's running
        manager.sendBroadcast(new Intent(PingableService.ACTION_PING));
        super.onStart();
    }

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

    protected BroadcastReceiver mReceiver = new BroadcastReceiver()
    {
        @Override
        public void onReceive (Context context, Intent intent)
        {
            // here you receive the response from the service
            if (intent.getAction().equals(PingableService.ACTION_PONG))
            {
                isSvcRunning = true;
            }
        }
    };
}

Прежде всего, вам не следует пытаться связаться с сервисом с помощью ActivityManager.(обсуждается здесь)

Службы могут запускаться самостоятельно, быть привязанными к действию или к тому и другому.Способ проверить в Activity, запущена ли ваша Служба или нет, — это создать интерфейс (расширяющий Binder), в котором вы объявляете методы, понятные как Activity, так и Службе.Вы можете сделать это, создав свой собственный интерфейс, в котором вы объявите, например, «isServiceRunning()».Затем вы можете привязать свою активность к своей Службе, запустить метод isServiceRunning(), Служба сама проверит, запущена она или нет, и вернет логическое значение в вашу активность.

Вы также можете использовать этот метод, чтобы остановить работу вашей Службы или взаимодействовать с ней другим способом.

я использовал это руководство чтобы узнать, как реализовать этот сценарий в моем приложении.

Версия Xamarin С#:

private bool isMyServiceRunning(System.Type cls)
{
    ActivityManager manager = (ActivityManager)GetSystemService(Context.ActivityService);

    foreach (var service in manager.GetRunningServices(int.MaxValue)) {
        if (service.Service.ClassName.Equals(Java.Lang.Class.FromType(cls).CanonicalName)) {
            return true;
        }
    }
    return false;
}

Для приведенного здесь варианта использования мы можем просто использовать stopService() возвращаемое значение метода.Он возвращает true если существует указанная служба и она убита.В противном случае он возвращается false.Таким образом, вы можете перезапустить службу, если результат false в противном случае можно быть уверенным, что текущая служба остановлена.:) Будет лучше, если вы посмотрите этот.

Опять же, еще одна альтернатива, которая может показаться людям более чистой, если они будут использовать ожидающие намерения (например, с помощью AlarmManager:

public static boolean isRunning(Class<? extends Service> serviceClass) {
    final Intent intent = new Intent(context, serviceClass);
    return (PendingIntent.getService(context, CODE, intent, PendingIntent.FLAG_NO_CREATE) != null);
}

Где CODE — это константа, которую вы определяете конфиденциально в своем классе для определения ожидающих намерений, связанных с вашей службой.

Ниже приведен элегантный хак, охватывающий все Ifs.Это только для местных служб.

    public final class AService extends Service {

        private static AService mInstance = null;

        public static boolean isServiceCreated() {
            try {
                // If instance was not cleared but the service was destroyed an Exception will be thrown
                return mInstance != null && mInstance.ping();
            } catch (NullPointerException e) {
                // destroyed/not-started
                return false;
            }
        }

        /**
         * Simply returns true. If the service is still active, this method will be accessible.
         * @return
         */
        private boolean ping() {
            return true;
        }

        @Override
        public void onCreate() {
            mInstance = this;
        }

        @Override
        public void onDestroy() {
            mInstance = null;
        }
    }

И потом позже:

    if(AService.isServiceCreated()){
        ...
    }else{
        startService(...);
    }

Ответ geekQ, но в классе Kotlin.Спасибо, geekQ

fun isMyServiceRunning(serviceClass : Class<*> ) : Boolean{
    var manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    for (service in manager.getRunningServices(Integer.MAX_VALUE)) {
        if (serviceClass.name.equals(service.service.className)) {
            return true
        }
    }
    return false
}

Вызов

isMyServiceRunning(NewService::class.java)

Может быть несколько сервисов с одним и тем же именем класса.

Я только что создал два приложения.Имя пакета первого приложения: com.example.mock.Я создал подпакет под названием lorem в приложении и сервисе под названием Mock2Service.Таким образом, его полное имя com.example.mock.lorem.Mock2Service.

Затем я создал второе приложение и сервис под названием Mock2Service.Имя пакета второго приложения: com.example.mock.lorem.Полное имя службы: com.example.mock.lorem.Mock2Service, слишком.

Вот мой вывод logcat.

03-27 12:02:19.985: D/TAG(32155): Mock-01: com.example.mock.lorem.Mock2Service
03-27 12:02:33.755: D/TAG(32277): Mock-02: com.example.mock.lorem.Mock2Service

Лучше всего сравнить ComponentName случаи, потому что equals() из ComponentName сравнивает имена пакетов и имена классов.И на устройстве не может быть двух приложений с одинаковым именем пакета.

Метод Equals() ComponentName.

@Override
public boolean equals(Object obj) {
    try {
        if (obj != null) {
            ComponentName other = (ComponentName)obj;
            // Note: no null checks, because mPackage and mClass can
            // never be null.
            return mPackage.equals(other.mPackage)
                    && mClass.equals(other.mClass);
        }
    } catch (ClassCastException e) {
    }
    return false;
}

ИмяКомпонента

Это больше относится к отладке Intent Service, поскольку они порождают поток, но может работать и для обычных сервисов.Я нашел эту тему благодаря Binging

В моем случае я поигрался с отладчиком и нашел представление потока.Это похоже на значок маркера в MS Word.В любом случае, вам не обязательно находиться в режиме отладчика, чтобы использовать его.Нажмите на процесс и нажмите на эту кнопку.Любые службы намерений будут отображаться во время их работы, по крайней мере, в эмуляторе.

Если служба принадлежит другому процессу или APK, используйте решение на основе ActivityManager.

Если у вас есть доступ к его источнику, просто используйте решение, основанное на статическом поле.Но вместо использования логического значения я бы предложил использовать объект Date.Пока служба работает, просто обновите ее значение до «сейчас», а когда она завершится, установите для него значение null.Из активности вы можете проверить, является ли ее значение нулевым или слишком старая дата, что будет означать, что она не запущена.

Вы также можете отправить широковещательное уведомление из своей службы, указав, что идет дополнительная информация, например прогресс.

Внутри TheServiceClass определите:

 public static Boolean serviceRunning = false;

Затем в onStartCommand(...)

 public int onStartCommand(Intent intent, int flags, int startId) {

    serviceRunning = true;
    ...
}

 @Override
public void onDestroy()
{
    serviceRunning = false;

} 

Затем позвоните if(TheServiceClass.serviceRunning == true) из любого класса.

простое использование связывания с не создавать автоматически - см. пс.и обновить...

public abstract class Context {

 ... 

  /*
  * @return {true} If you have successfully bound to the service, 
  *  {false} is returned if the connection is not made 
  *  so you will not receive the service object.
  */
  public abstract boolean bindService(@RequiresPermission Intent service,
        @NonNull ServiceConnection conn, @BindServiceFlags int flags);

пример :

    Intent bindIntent = new Intent(context, Class<Service>);
    boolean bindResult = context.bindService(bindIntent, ServiceConnection, 0);

почему бы не использовать? getRunningServices()

List<ActivityManager.RunningServiceInfo> getRunningServices (int maxNum)
Return a list of the services that are currently running.

Примечание:этот метод предназначен только для отладки или реализации пользовательских интерфейсов типа управления службами.


пс.Документация по Android вводит в заблуждение. Я открыл проблему в трекере Google, чтобы устранить любые сомнения:

https://issuetracker.google.com/issues/68908332

как мы видим, служба привязки фактически вызывает транзакцию через привязку ActivityManager через привязки кэша службы - я не отслеживаю, какая служба отвечает за привязку, но, как мы видим, результат привязки следующий:

int res = ActivityManagerNative.getDefault().bindService(...);
return res != 0;

транзакция осуществляется через связующее:

ServiceManager.getService("activity");

следующий:

  public static IBinder getService(String name) {
    try {
        IBinder service = sCache.get(name);
        if (service != null) {
            return service;
        } else {
            return getIServiceManager().getService(name);

это устанавливается в ActivityThread через:

 public final void bindApplication(...) {

        if (services != null) {
            // Setup the service cache in the ServiceManager
            ServiceManager.initServiceCache(services);
        }

это вызывается в ActivityManagerService в методе:

 private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {
    ...
    thread.bindApplication(... , getCommonServicesLocked(),...)

затем:

 private HashMap<String, IBinder> getCommonServicesLocked() {

но никакой "активности" нет, только пакет окон и сигнализация..

поэтому нам нужно вернуться и позвонить:

 return getIServiceManager().getService(name);

    sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());

это вызывает вызов через:

    mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);

что приводит к :

BinderInternal.getContextObject()

и это родной метод....

  /**
     * Return the global "context object" of the system.  This is usually
     * an implementation of IServiceManager, which you can use to find
     * other services.
     */
    public static final native IBinder getContextObject();

У меня сейчас нет времени копаться в c, поэтому, пока я не проанализирую rest call, я приостанавливаю свой ответ.

но лучший способ проверить, работает ли служба состоит в том, чтобы создать привязку (если привязка не создана, служба не существует) - и запросить службу о ее состоянии через привязку (используя сохраненный внутренний флаг для ее состояния).

обновление 23.06.2018

мне показалось это интересным:

/**
 * Provide a binder to an already-bound service.  This method is synchronous
 * and will not start the target service if it is not present, so it is safe
 * to call from {@link #onReceive}.
 *
 * For peekService() to return a non null {@link android.os.IBinder} interface
 * the service must have published it before. In other words some component
 * must have called {@link android.content.Context#bindService(Intent, ServiceConnection, int)} on it.
 *
 * @param myContext The Context that had been passed to {@link #onReceive(Context, Intent)}
 * @param service Identifies the already-bound service you wish to use. See
 * {@link android.content.Context#bindService(Intent, ServiceConnection, int)}
 * for more information.
 */
public IBinder peekService(Context myContext, Intent service) {
    IActivityManager am = ActivityManager.getService();
    IBinder binder = null;
    try {
        service.prepareToLeaveProcess(myContext);
        binder = am.peekService(service, service.resolveTypeIfNeeded(
                myContext.getContentResolver()), myContext.getOpPackageName());
    } catch (RemoteException e) {
    }
    return binder;
}

суммируя :)

«Предоставьте привязку к уже привязанному сервису.Этот метод является синхронным и не запустит целевую службу, если она отсутствует».

Public Ibinder Peekservice (Service Service, String ResolvedType, String CallingPackage) бросает remoteexception;

*

public static IBinder peekService(IBinder remote, Intent service, String resolvedType)
             throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken("android.app.IActivityManager");
    service.writeToParcel(data, 0);
    data.writeString(resolvedType);
    remote.transact(android.os.IBinder.FIRST_CALL_TRANSACTION+84, data, reply, 0);
    reply.readException();
    IBinder binder = reply.readStrongBinder();
    reply.recycle();
    data.recycle();
    return binder;
}

*

В подклассе службы используйте статическое логическое значение, чтобы получить состояние службы, как показано ниже.

MyService.kt

class MyService : Service() {
    override fun onCreate() {
        super.onCreate()
        isServiceStarted = true
    }
    override fun onDestroy() {
        super.onDestroy()
        isServiceStarted = false
    }
    companion object {
        var isServiceStarted = false
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val serviceStarted = FileObserverService.isServiceStarted
        if (!serviceStarted) {
            val startFileObserverService = Intent(this, FileObserverService::class.java)
            ContextCompat.startForegroundService(this, startFileObserverService)
        }
    }
}

Мое преобразование Kotlin ActivityManager::getRunningServices основанные ответы.Поместите эту функцию в Activity-

private fun isMyServiceRunning(serviceClass: Class<out Service>) =
    (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
        .getRunningServices(Int.MAX_VALUE)
        ?.map { it.service.className }
        ?.contains(serviceClass.name) ?: false

Пожалуйста, используйте этот код.

if (isMyServiceRunning(MainActivity.this, xyzService.class)) { // Service class name
    // Service running
} else {
    // Service Stop
}


public static boolean isMyServiceRunning(Activity activity, Class<?> serviceClass) {
        ActivityManager manager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (serviceClass.getName().equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

Ребята, успокойтесь...:)

Я думаю, что наиболее подходящим решением является хранение пары ключ-значение в SharedPreferences о том, работает ли служба или нет.

Логика очень прямая;на любую желаемую должность в вашем классе обслуживания;поместите логическое значение, которое будет служить для вас флагом того, запущена служба или нет.Затем прочитайте это значение в любом месте вашего приложения.

Пример кода, который я использую в своем приложении, приведен ниже:

В моем классе службы (служба для аудиопотока) я выполняю следующий код, когда служба работает;

private void updatePlayerStatus(boolean isRadioPlaying)
{
        SharedPreferences sharedPref = this.getSharedPreferences(getString(R.string.str_shared_file_name), Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putBoolean(getString(R.string.str_shared_file_radio_status_key), isRadioPlaying);
        editor.commit();
}

Затем в любой деятельности моего приложения я проверяю статус службы с помощью следующего кода:

private boolean isRadioRunning() {
        SharedPreferences sharedPref = this.getSharedPreferences(getString(R.string.str_shared_file_name), Context.MODE_PRIVATE);

        return sharedPref.getBoolean(getString(R.string.str_shared_file_radio_status_key), false);
}

Никаких специальных разрешений, никаких циклов...Простой способ, чистое решение :)

Если вам нужна дополнительная информация, пожалуйста, обратитесь к связь

Надеюсь это поможет.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top