Status Update
Comments
il...@google.com <il...@google.com> #2
Also crashes in R8 version 8.5.35, 8.6.17, 8.7.2-dev
ac...@google.com <ac...@google.com>
sg...@google.com <sg...@google.com> #3
You cannot apply keep rules for classes synthesized by R8, but if you keep the implemented interfaces, the merging should not happen.
That said, checking the implementing class of a lambda might not be a good idea. The specification for java.lang.invoke.LambdaMetafactory
Capture may involve allocation of a new function object, or may return a suitable existing function object. The identity of a function object produced by capture is unpredictable, and therefore identity-sensitive operations (such as reference equality, object locking, and System.identityHashCode()) may produce different results in different implementations, or even upon different invocations in the same implementation.
If I understand this correctly the only guarantee is that the function object implements the interface(s) specified.
[Deleted User] <[Deleted User]> #4
The actual problem is that R8 deletes FunInterface1 and FunInterface2 classes, which are used for runtime di:
fun hello() {
val instance1 = FunInterface1 { println("Doing business") }
val instance2 = FunInterface2 { println("Starting the show") }
val map = mapOf(
FunInterface1::class.java to instance1,
FunInterface2::class.java to instance2,
)
check(map.size == 2) { "Map size is ${map.size}" }
}
So do I have to list all fun interfaces in my sdk using -keep? I might do that for ~100 of them that exist right now, but this won't save future developers from adding another fun interface and forgetting to list them in .pro file.
sg...@google.com <sg...@google.com> #5
Not sure what the use case is here. However, if you want the interfaces to stay as is you will have to have a -keep
rule for them. One option to handle this without adding a rule for each interface could be to annotate them and add a single rule for all interfaces annotated by that annotation.
[Deleted User] <[Deleted User]> #6
Our use case is to create a static in-memory Di object, to put and get instances from all over the multi-module application. It kinda looks like
class DiContainer(bindings: Map<Class<*>, SingletonBinding<*>>) {
private val bindings: Map<Class<*>, SingletonBinding<*>> = HashMap(bindings)
fun <T : Any> instance(typeSpec: Class<*>): T {
@Suppress("UNCHECKED_CAST")
val result: T = (bindings[typeSpec]?.lazy?.value) as? T ?: error("No binding for $typeSpec found.")
return result
}
}
class SingletonBinding<out T : Any>(private val singleton: () -> T) {
val lazy = lazy { singleton() }
}
class DiScope {
val bindings: MutableMap<Class<*>, SingletonBinding<*>> = HashMap()
fun addBinding(typeSpec: Class<*>, binding: SingletonBinding<*>) {
check(typeSpec !in bindings) { "$typeSpec already registered." }
bindings[typeSpec] = binding
}
}
open class DiHolder {
@Volatile
@PublishedApi
internal var instance: DiContainer? = null
protected fun build(init: DiScope.() -> Unit) {
check(instance == null) { "Di already initialized" }
instance = DiContainer(DiScope().apply(init).bindings)
}
inline fun <reified T : Any> instance(): T = instance(T::class.java)
fun <T : Any> instance(typeSpec: Class<*>): T = instance!!.instance(typeSpec)
}
object Di : DiHolder() {
operator fun invoke(init: DiScope.() -> Unit) = build(init)
}
Now, when we use this, for example:
fun hello() {
Di {
addBinding(FunInterface1::class.java, SingletonBinding { FunInterface1 { println("Doing business") } })
addBinding(FunInterface2::class.java, SingletonBinding { FunInterface2 { println("Doing business") } })
}
// In some other place in code, but in the same process
Di.instance<FunInterface1>().doBusiness()
Di.instance<FunInterface2>().startTheShow()
}
Now we can get an interface instance from anywhere in the app, because Di is singleton. However, when running release with R8 full mode, I get this exception:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.r8/com.example.r8.MainActivity}: java.lang.IllegalStateException: class com.example.r8.MainActivityKt$$ExternalSyntheticLambda2 already registered.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3782)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8176)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.IllegalStateException: class com.example.r8.MainActivityKt$$ExternalSyntheticLambda2 already registered.
at com.example.r8.DiScope.addBinding(Unknown Source:37)
at com.example.r8.MainActivity.onCreate(SourceFile:3)
at android.app.Activity.performCreate(Activity.java:8595)
at android.app.Activity.performCreate(Activity.java:8573)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8176)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
sg...@google.com <sg...@google.com> #7
I also tried koin, and somehow their code arrangement dodges the type check, and everything works.
Oh, I see now, by default their
// These are reduced InsertKoinIO/koin sources
class InstanceRegistry {
val instances = ConcurrentHashMap<String, SingleInstanceFactory<*>>()
fun saveMapping(mapping: String, factory: SingleInstanceFactory<*>) {
if (instances.containsKey(mapping)) {
throw Exception("Already existing definition for ${factory.beanDefinition} at $mapping")
}
instances[mapping] = factory
}
inline fun <reified T> instance(): T {
return instances[T::class.getFullName()]?.get() as T
}
}
class SingleInstanceFactory<T>(val beanDefinition: () -> T) {
private var value: T? = null
private fun getValue(): T = value ?: error("Single instance created couldn't return value")
fun get(): T {
synchronized(this) {
if (value == null) {
value = beanDefinition.invoke()
}
}
return getValue()
}
}
fun KClass<*>.getFullName(): String = this.java.name
/// --------------
fun helloKoin() {
val koin = InstanceRegistry()
val shouldR8CrashTheApp = true
if (shouldR8CrashTheApp) {
koin.saveMapping(FunInterface1::class.getFullName(), SingleInstanceFactory { FunInterface1 { println("Do business") } })
koin.saveMapping(FunInterface2::class.getFullName(), SingleInstanceFactory{ FunInterface2 { println("Start the show") } })
} else {
// because in runtime FunInterfaces are clamped into one class, only one saveMapping is called, and everything works
val mappings = hashMapOf<String, SingleInstanceFactory<*>>()
mappings[FunInterface1::class.getFullName()] = SingleInstanceFactory { FunInterface1 { println("Do business") } }
mappings[FunInterface2::class.getFullName()] = SingleInstanceFactory { FunInterface2 { println("Start the show") } }
mappings.forEach { (mapping, factory) ->
koin.saveMapping(mapping, factory)
}
}
koin.instance<FunInterface1>().doBusiness()
koin.instance<FunInterface2>().startTheShow()
}
[Deleted User] <[Deleted User]> #8
It would also be nice, if FunInterface1::class.java == FunInterface2::class.java
and setOf(FunInterface1::class.java, FunInterface2::class.java).size == 2
Description
Hilt
KSP
Fragment KTX
Activity KTX
ViewModels
viewModels() delegate
R8 -> proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
Version used:
Hilt -> 2.51.1
KSP -> 2.0.0-1.0.21
Fragment KTX -> 1.6.0-alpha03
Activity KTX -> 1.7.0-alpha01
Devices/Android versions reproduced on:
Android Emulators & Samsung A03
Brief explanation:
While we run our app´s release build type which has the already mentioned R8 config, hilt builts the proper ViewModel Providers, then the following crash appears (de-obfuscated code):
Fatal Exception: java.lang.IllegalArgumentException: Multiple entries with same key: Ma.v=true and Ma.v=true
at com.google.common.collect.ImmutableMap$Builder$DuplicateKey.java.lang.IllegalArgumentException exception()(SourceFile:45)
at com.google.crypto.tink.subtle.prf.HkdfStreamingPrf.com.google.common.collect.ImmutableMap com.google.common.collect.ImmutableMap$Builder.build(boolean)(SourceFile:433)
com.google.common.collect.ImmutableMap com.google.common.collect.ImmutableMap$Builder.buildOrThrow()
at com.xxx.mobile.DaggerxxxMobileApplication_HiltComponents_SingletonC$ActivityCImpl.com.google.common.collect.ImmutableMap com.google.common.collect.ImmutableMap$Builder.build()(SourceFile:296)
java.util.Map getViewModelKeys()
at com.google.android.gms.internal.mlkit_vision_barcode.zzef.dagger.hilt.android.internal.lifecycle.DefaultViewModelFactories$InternalFactoryFactory com.xxx.mobile.DaggerxxxMobileApplication_HiltComponents_SingletonC$ActivityCImpl.getHiltInternalFactoryFactory()(SourceFile:13)
dagger.hilt.android.internal.lifecycle.DefaultViewModelFactories$InternalFactoryFactory com.xxx.mobile.DaggerxxxMobileApplication_HiltComponents_SingletonC$FragmentCImpl.getHiltInternalFactoryFactory()
androidx.lifecycle.ViewModelProvider$Factory dagger.hilt.android.internal.lifecycle.DefaultViewModelFactories.getFragmentFactory(androidx.fragment.app.Fragment,androidx.lifecycle.ViewModelProvider$Factory)
at com.xxx.mobile.splash.SplashFragment.androidx.lifecycle.ViewModelProvider$Factory com.xxx.mobile.splash.Hilt_SplashFragment.getDefaultViewModelProviderFactory()(SourceFile:5)
at com.xxx.mobile.login.tenant.TenantFragment$special$$inlined$viewModels$default$10.androidx.lifecycle.ViewModelProvider$Factory com.xxx.mobile.splash.SplashFragment$special$$inlined$viewModels$default$5.invoke()(SourceFile:132)
java.lang.Object com.xxx.mobile.splash.SplashFragment$special$$inlined$viewModels$default$5.invoke()
at com.xxx.mobile.support.database.dao.BrandsDAO_Impl.androidx.lifecycle.ViewModel androidx.lifecycle.ViewModelLazy.getValue()(SourceFile:21)
java.lang.Object androidx.lifecycle.ViewModelLazy.getValue()
at com.xxx.mobile.splash.SplashFragment.com.xxx.mobile.splash.SettingsViewModel getViewModel()(SourceFile:11)
void onViewCreated(android.view.View,android.os.Bundle)
at androidx.fragment.app.Fragment.void performViewCreated()(SourceFile:15)
at androidx.fragment.app.FragmentStateManager.void createView()(SourceFile:299)
at androidx.fragment.app.FragmentStateManager.void moveToExpectedState()(SourceFile:175)
at androidx.fragment.app.FragmentManager.void executeOpsTogether(java.util.ArrayList,java.util.ArrayList,int,int)(SourceFile:1166)
at androidx.fragment.app.FragmentManager.void removeRedundantOperationsAndExecute(java.util.ArrayList,java.util.ArrayList)(SourceFile:82)
at androidx.fragment.app.FragmentManager.boolean execPendingActions(boolean)(SourceFile:78)
at androidx.fragment.app.FragmentManager.void dispatchStateChange(int)(SourceFile:65)
at androidx.fragment.app.FragmentActivity.void androidx.fragment.app.FragmentManager.dispatchActivityCreated()(SourceFile:34)
void androidx.fragment.app.FragmentController.dispatchActivityCreated()
void onStart()
at androidx.appcompat.app.AppCompatActivity.void onStart()(SourceFile:1)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1701)
at android.app.Activity.performStart(Activity.java:9023)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:4072)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:270)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:250)
at android.app.servertransaction.TransactionExecutor.executeLifecycleItem(TransactionExecutor.java:222)
at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:107)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:81)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2635)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:317)
at android.app.ActivityThread.main(ActivityThread.java:8699)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
we´d tried with check whether it's the dependencies that a View Model takes, the type, but it doesn´t matter
also providing a custom factory solves the issue.
Anything else we can provide let us know.
Thank you.