Service onCreate 在 Application onCreate 之前调用。

4

最近版本的应用在开发者控制台报告了一些崩溃问题,让我感到很疯狂。

它们表现为java.lang.IllegalStateException,似乎是Service.onCreate在Application.onCreate之前调用导致的。

这种情况只会在0.3%的用户设备上出现,而且仅限于Android 8设备。我自己的设备无法复制该问题。

我将更好地解释发生了什么。 App以如下方式扩展Application:

public class MySpecificApp  extends MyBaseApp
{
    static
    {
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    }

    public void onCreate()
    {
        super.onCreate();

        ...  // Specific initializations
    }

    ...
}

public class MyBaseApp  extends Application
{

    private static MyBaseApp smApplication;

    public void onCreate()
    {
        super.onCreate();

        // Fabric (Crashlitics) initialization. It SHOULD NOT BE needed, just to be sure (tried also without this line)
        Fabric.with(this, new Crashlytics());

        MyBaseApp.smApplication = this;             // smApplication is initialized here and NEVER modified

        ... // Base initializations     
    }

    public static MyBaseApp getApp() {
        return MyBaseApp.smApplication;
    }

    ...
}

在 MyService.onCreate 方法中,对 MyBaseApp.smApplication 进行检查,发现 MyBaseClass.onCreate 从未被调用过。
public class MyService extends Service
{

    public MyService()
    {
    }

    @Override
    public void onCreate()
    {
        try
        {
            if( MySpecificApp.getApp() == null )
                Crashlytics.log("MyService.onCreate: probably Application.onCreate not yet called");        <=== Here is raised the Exception
            // The line over is the line 617


            ... // Service initializations

        }
        catch( Exception e )
        {
            e.printStackTrace();

            throw e;
        }
    }

    ...
}

实际上,Crashlytics崩溃是因为它尚未初始化,但这是因为应用程序尚未初始化。

Crashlytics不是关键因素。代码采取这种方式是因为我遇到了几个IllegalStateException奇怪的错误,并且我认为缺少应用程序初始化:我正在尝试调查它。

以下是开发者控制台的堆栈日志(崩溃未到达Crashlytics):

java.lang.RuntimeException: 
  at android.app.ActivityThread.handleCreateService (ActivityThread.java:3554)
  at android.app.ActivityThread.-wrap4 (Unknown Source)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1786)
  at android.os.Handler.dispatchMessage (Handler.java:105)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:6944)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:327)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1374)
Caused by: java.lang.IllegalStateException: 
  at io.fabric.sdk.android.Fabric.singleton (Fabric.java:301)
  at io.fabric.sdk.android.Fabric.getKit (Fabric.java:551)
  at com.crashlytics.android.Crashlytics.getInstance (Crashlytics.java:191)
  at com.crashlytics.android.Crashlytics.checkInitialized (Crashlytics.java:390)
  at com.crashlytics.android.Crashlytics.log (Crashlytics.java:221)
  at com.xxx.MyService.onCreate (MyService.java:617)                                <==== Here is the line that is reached only if Application.onCreate is not called
  at android.app.ActivityThread.handleCreateService (ActivityThread.java:3544)
  at android.app.ActivityThread.-wrap4 (Unknown Source)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1786)
  at android.os.Handler.dispatchMessage (Handler.java:105)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:6944)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:327)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1374)

这里是清单的摘录:
<application
    android:name=".MySpecificApp"
    android:allowBackup="true"
    android:fullBackupContent="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme"
    android:largeHeap="true">

    ...

    <service
        android:name=".MyService"
        android:enabled="true"
        android:exported="false"
        android:stopWithTask="false">
    </service>

    ...
</application>

有人知道这个 bug 是怎么产生的吗?该如何修复它呢?


这个服务是 STICKY 吗? - Deˣ
服务不会“消耗”意图,它会自行管理生命周期并在需要时调用stopSelf()。实际上,它根据某些条件返回START_REDELIVER_INTENT或START_NOT_STICKY。无论如何,在创建新服务之前没有创建应用程序是没有意义的。@Derek,您是否知道某些情况下的一些案例? - ARLabs
@ARLabs,您能否在代码中发布有关服务启动位置的相关代码? - Elli White
@ElliWhite,我认为在启动服务的方式上没有什么相关的内容。在JobService的onStartJob中调用startService或startForegroundService(根据某些条件),或者直接在Service的代码中调用。可以多次调用startService,当不再需要保持活动状态时调用stopSelf。 - ARLabs
@ARLabs 我非常不同意那里没有任何相关内容。这个错误与您启动服务的方式有关,因此我认为启动服务绝对与解决此问题相关。 - Elli White
4个回答

4
我不知道为什么你的服务在调用的之前启动,特别是因为你实现的服务缺少大量代码。你是如何启动它的?你的目标API是什么?是否有某个设备/制造商会导致这种情况?虽然这不是我在Android上看到的最奇怪的事情,但也有可能是操作系统中的错误,如果仅仅是Android 8且在其他版本上没有发生过。
也许有一些方法可以解决:
方案A
首先,在应用程序启动时创建的不是 (有点反直觉)。据我所知, 是首先被创建的组件(这就是为什么某些服务如Firebase使用它们来设置一些东西,例如崩溃报告)。
在创建时,也不是第一个被调用的函数(更加反直觉)。在创建后立即调用init函数/构造函数。任何不需要的工作都可以在默认构造函数中完成。接下来被调用的是函数。这是您可以使用运行初始化的最早时间。此时创建(构造函数+)。只有在此之后,才会调用的函数。
请查看下面的示例:
class MyApp : Application() {

    init {
        Log.i("MyApp", "init")
    }

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        Log.i("MyApp", "attachBaseContext")
    }

    override fun onCreate() {
        super.onCreate()
        Log.i("MyApp", "onCreate")
    }
}

class MyContentProvider : ContentProvider() {

    init {
        Log.i("MyContentProvider", "init")
    }

    override fun onCreate(): Boolean {
        Log.i("MyContentProvider", "onCreate")
        return true
    }

    ...

}

如果您启动此应用程序,则会获得以下输出:

I/MyApp: init
I/MyApp: attachBaseContext
I/MyContentProvider: init
I/MyContentProvider: onCreate
I/MyApp: onCreate

您可以利用这一事实将关键的初始化移动到例如ContentProviderattachBaseContext方法中,并尝试超过早期创建的Service实例。 方法B 另一个想法是在发生错误的情况下手动延迟Service初始化。由于这只会在所有情况中发生0.3%,因此我认为稍微延迟一下以防止崩溃是可以接受的。您可以执行以下操作(对于我的Kotlin表示抱歉):
fun onCreate() {
    super.onCreate()
    onCreate0()
}

private fun onCreate0() {
    if (MySpecificApp.getApp() == null) {
        Log.w("MyService", "Delaying service initialization due to race condition")
        Handler(Looper.getMainLooper()).postDelayed(this::onCreate0, 100)
        return
    } 

    Log.i("MyService", "Application init verified, initializing service now")
    // Do your stuff here. Application is initialized
}



基本上,你要进行检查,如果应用程序尚未准备好,则在100毫秒后重试。这不是完美的解决方案,但比崩溃要好。建议仍然收集有关此行为的数据,以便A.查看需要多少次循环,B.这种情况发生的频率以及哪些设备上发生。可能也可以通过类似Firebase远程配置之类的东西来控制最大尝试次数。由于应用程序处于奇怪的初始化状态,获取云值可能会失败,因此......
通过这样做,您还可以收集有关此行为的更多见解。应用程序是否已完全初始化(这就是为什么我要限制循环次数的原因)?
方法B v2 为了解决您对此答案评论的担忧,这里是一个更新版本,处理意图。
在您的服务中进行相同的检查,但缓存无法处理的“Intent”。服务将停止自己。
var isInitialized = false

// static in Java
companion object {
    private val pendingIntents = mutableListOf<Intent>()

    fun hasPendingIntents() = pendingIntents.size > 0
}

fun onCreate() {
    super.onCreate()
    if (MySpecificApp.getApp() == null) {
        Log.w("MyService", "Application not ready")
        stopSelf()
        return
    } 

    Log.i("MyService", "Application init verified, initializing service now")
    isInitialized = true
    // Do your stuff here. Application is initialized
}

fun onStartCommand(intent: Intent, flags: Int, startId: Int) {
    pendingIntents.add(intent)

    if (!isInitialized) {
        return   
    }

    pendingIntents.forEach {
        // Handle intents which could not be satisfied before including the latest
    }

    pendingIntents.clear()

    return super.onStartCommand(intent, flags, startId)
}


在您的应用程序中检查服务是否遇到问题,如果是,则手动启动它:

fun onCreate() {
    super.onCreate()
    // init app
    if (MyService.hasPendingIntents()) {
       startService(Intent(this, MyService::class).putExtra("RECOVER_AFTER_FAILURE", true)
    }
}

方案C

这不是一种解决方案,但可以让您更接近“清洁”的解决方案:您可以尝试将最近的100个日志缓存到磁盘上(应用程序创建、活动状态等),当服务启动时将这100个日志附加到崩溃报告中。这可能会提供有价值的见解,例如,应用程序是否最近被使用过,还是长时间处于闲置/关闭状态。也许您会找到一些指向正确方向的线索。了解发生此行为的情况将非常有趣。


感谢对方法A的解释,我知道ContentProvider是在Application之前创建的,但我没有任何CP。我也知道构造函数先执行...但问题不仅在我的初始化代码中...而且在Application本身中也存在问题。在确定这个问题之前,我遇到了其他我无法理解的IllegalStateExceptions...现在我明白那是由于onCreate调用缺失引起的。即使Crashlitics在这种情况下也无法工作。 - ARLabs
关于方案B,这可能是一个不错的建议...我在其他情况下使用了这种方法...但在这种情况下可能会更困难...例如,在Service onCreate之后,我将获得带有Intent的onStartComment,它无法运行。应该也要延迟它...但如果应用程序没有正确初始化,它会顺利进行吗?这需要深入测试...而且我无法重现这种情况!在这种情况下,测试确实很困难。 - ARLabs
关于 Approach C,您建议如何发送日志?在这种情况下,我无法使用 Crashlitics,而 Developer 控制台上的崩溃仅报告异常和堆栈... - ARLabs
1
A: “即使Crashlitics在这种情况下也无法工作”意味着甚至ContentProviders都没有初始化。很奇怪。 B: 我添加了一个不同版本的B。我们还可以缓存Intents并在应用程序准备好后处理它们。 C: 我个人使用类似Timber的日志记录工具。在Timber中,您可以添加一个自定义树形结构,该结构可以缓存在磁盘上,例如SharedPreferences。当问题发生时,您也将异常存储在磁盘上,然后让其崩溃。每次启动时,您都会检查磁盘缓存以获取该异常,如果存在,则获取日志和异常,并将它们放入Crashlytics中。 - crysxd
根据Application#onCreate,在任何活动、服务或接收器对象(不包括内容提供者)被创建之前,在应用程序启动时调用。https://developer.android.com/reference/android/app/Application.html#onCreate()这确实不是预期的行为。 - H4SN

0

你的服务中有以下代码行:

android:stopWithTask="false"

如果设置为true,则该服务将在用户删除由应用程序拥有的根任务时自动停止。默认值为false。
我建议将其设置为true,因为您不希望即使关闭了应用程序拥有的任务也要运行服务。还要检查您的服务是否为STICKY。

我不认为这是原因。我已经使用了那个设置好几年了,我需要服务保持活动状态...因为它的操作可能会相当长(数十秒),我不希望它重新启动...有些用户习惯于每次退出应用程序时都杀死它们。无论如何,无论是保持活动状态还是重新发送意图,在创建服务之前必须创建应用程序。无论如何,我可以进行一些测试来删除它。 - ARLabs
1
@ARLabs 只是好奇,如果您无法在自己的设备上重现问题,并且发生率为0.3%,您将如何进行测试? - Abbas
@Abbas 嗯...我发布了一个新的测试版,进行了修改,并等待一周的崩溃报告。 - ARLabs

0

后台执行限制

Android 8.0 行为变更

作为Android 8.0 (API 级别 26)为改善电池寿命而引入的变更之一,当您的应用程序进入缓存状态且没有活动组件时,系统会释放应用程序持有的任何唤醒锁。

此外,为了提高设备性能,系统限制了非前台运行的应用程序的某些行为。具体来说:

正在后台运行的应用程序现在对其自由访问后台服务的限制更加严格。应用程序不能使用其清单来注册大多数隐式广播(即不是专门针对该应用程序的广播)。默认情况下,这些限制仅适用于目标为“O”的应用程序。但是,用户可以从设置屏幕启用这些限制,即使该应用程序尚未以“O”为目标。

Android 8.0

(API 级别 26)还包括对特定方法的以下更改:

startService() 方法现在会抛出 IllegalStateException 异常,如果一个针对 Android 8.0 的应用程序试图在不允许创建后台服务的情况下使用该方法。 新的 Context.startForegroundService() 方法启动前台服务。系统允许应用程序在后台调用 Context.startForegroundService() 方法。但是,在服务创建后五秒钟内,应用程序必须调用该服务的 startForeground() 方法。 有关更多信息,请参见 Background Execution Limits。


-1

从Android 8开始,任何非粘性服务在后台超过几分钟(通常为1分钟)后将被销毁。一旦用户再次回到前台,如https://developer.android.com/about/versions/oreo/background#services所述,它将被重新创建(服务的onCreate将被调用)。

当应用程序处于前台时,它可以自由地创建和运行前台和后台服务。当应用程序进入后台时,它有几分钟的时间窗口仍然允许其创建和使用服务。在该窗口结束时,应用程序被视为空闲状态。此时,系统停止应用程序的后台服务,就像应用程序调用了服务的Service.stopSelf()方法一样。

我记不清是只有服务被杀死还是整个应用程序都被杀死了,但无论如何,如果服务在后台超过1分钟并被销毁,你的应用程序可能会因为其他应用程序请求更多内存RAM而死亡。虽然你的应用程序被杀死了,但它仍然保留在最近使用的应用程序列表中。

当用户返回您的应用程序时,将调用应用程序类的onCreate()方法和您的服务的onCreate()方法以重新创建它。我无法确定哪个onCreate()方法会首先被触发。

如果您不知道如何在后台杀死应用程序,请使用:enter image description here

尽管如此,如果这不是您的问题,您可以通过使用getApplicationContext()并将其转换为MyBaseApp来从Service访问应用程序。无需访问MyBaseApp上的静态方法。这种方式不会失败,因为Service扩展了Context,因此您始终可以将其强制转换为您的Application。

public class MyService extends Service {
    private MyBaseApp application;
    @Override
    public void onCreate() {
        application = ((MyBaseApp) getApplicationContext())
    }
    ...
}

我了解Android8的后台前台问题,并且我已经妥善处理它们(希望是正确的)。在Service之前必须始终调用Application onCreate方法(https://developer.android.com/reference/android/app/Application.html#onCreate())。 - ARLabs
我之前使用过getApplicationContext...但是在这种情况下我得到了null。静态的smApplication只是帮助我理解Application onCreate没有被调用。 - ARLabs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接