Utilizing lambda as callback is quite common in Jetpack compose, however have you learnt you may as well use perform reference to interchange the lambda?
Once I first heard about this perform reference, I believed the perform reference should be higher. Not like nameless perform / lambda, it only a perform pointer or reference to a perform. It ought to run quicker and use much less reminiscence, because it would not allocate further reminiscence. So I changed lambda with perform reference each time is feasible. Effectively, I used to be improper!
After performing some researches, it seems that perform reference nonetheless allocates further reminiscence identical to what lambda does, and it additionally runs 2x slower than lambda. Earlier than we glance into why, let’s perceive their usages and variations first with examples beneath.
Lambda (name the item’s perform)
As an example you could have Display screen
and ViewModel
courses beneath. The id
is used to determine a unique occasion of ViewModel
.
class Display screen(personal val callback: () -> Unit) {
enjoyable onClick() = callback()
}
class ViewModel(var id: String) {
enjoyable screenOnClick() {
println("ViewModel$id onClick() known as!")
}
enjoyable screenOnClickDoNothing() {}
}
In predominant()
, it creates the ViewModel
object. Then, you go within the lambda callback parameter – { viewModel.screenOnClick() }
to Display screen
‘s constructor.
enjoyable predominant() {
var viewModel = ViewModel("1")
val display = Display screen {
viewModel.screenOnClick()
}
viewModel = ViewModel("2")
display.onClick()
}
viewModel.screenOnClick()
is the item’s perform.
To reveal the distinction between lambda and performance reference, you replace the viewModel
variable with one other new occasion of ViewModel
– viewModel = ViewModel("2")
earlier than calling display.onClick()
.
Calling
display.onClick()
is to simulate display callback
Right here is the output:
ViewModel2 onClick() known as!
Please be aware that ViewModel2 known as as an alternative of ViewModel1. It appears to be like like lambda holds the reference to the viewModel
variable. When the variable is up to date, it’s mechanically mirrored.
Object’s Operate Reference
Let’s change the lambda with a perform reference – viewModel::screenOnClick
. The code appears to be like like this.
enjoyable predominant() {
var viewModel = ViewModel("1")
val display = Display screen(callback = viewModel::screenOnClick)
viewModel = ViewModel("2")
display.onClick()
}
Right here is the output
ViewModel1 onClick() known as!
Please be aware that ViewModel1 known as as an alternative of ViewModel2. It appears to be like just like the perform reference is caching the ViewModel
occasion when it’s known as. Thus, it’s the previous ViewModel
occasion and never the up to date one. Improper!
In the event you do not change the viewModel
occasion however modifying ViewModel.id
immediately as an alternative,
enjoyable predominant() {
var viewModel = ViewModel("1")
val display = Display screen(callback = viewModel::screenOnClick)
viewModel.id = "2"
display.onClick()
}
you get the up to date worth.
ViewModel2 onClick() known as!
This tells you that perform reference would not cache the ViewModel
occasion, however holding its reference. If the unique viewModel
is changed, the Display screen
nonetheless holds the previous reference of the ViewModel
. Thus, it prints out the worth of that previous reference.
Efficiency Take a look at (Object’s Operate)
Let’s run some efficiency checks.
That is the lambda check that creating Display screen
1 billions instances.
enjoyable lambdaTest() {
val viewModel = ViewModel("1")
val timeMillis = measureTimeMillis {
repeat(1_000_000_000) {
val display = Display screen {
viewModel.screenOnClickDoNothing()
}
display.onClick()
}
}
println("lambdaTest: ${timeMillis/1000f} seconds")
}
The output present it runs ~2.4 seconds.
lambdaTest: 2.464 seconds
Now, it’s perform reference’s flip.
enjoyable funRefTest() {
val viewModel = ViewModel("1")
val timeMillis = measureTimeMillis {
repeat(1_000_000_000) {
val display = Display screen(
callback = viewModel::screenOnClickDoNothing
)
display.onClick()
}
}
println("funRefTest: ${timeMillis/1000f} seconds")
}
The output reveals it runs 5.2 seconds.
funRefTest: 5.225 seconds
This concludes that lambda runs ~2x quicker than perform reference. Why? Let us take a look at the decompiled code.
Decompiled Code In Java
Let us take a look at the decompiled code in Java. That is the simplified model with out the measureTimeMillis
and repeat
code.
lambdaTest()
public static closing void lambdaTest() {
closing ViewModel viewModel = new ViewModel();
Display screen display = new Display screen((Function0)(new Function0() {
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public closing void invoke() {
viewModel.screenOnClickDoNothing();
}
}));
display.onClick();
}
funRefTest()
public static closing void funRefTest() {
ViewModel viewModel = new ViewModel();
Display screen display = new Display screen((Function0)(new Function0(viewModel) {
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public closing void invoke() {
((ViewModel)this.receiver).screenOnClickDoNothing();
}
}));
display.onClick();
}
Few necessary issues right here:
- Operate reference allocates new perform reminiscence identical to the lambda –
new Function0()
andnew Function0(viewModel)
. So it isn’t a perform pointer. - Operate reference passes within the
viewModel
intoFunction0
and lambda would not
Based mostly on these findings, I think the passed-in ViewModel
reference might be the overhead (requires further copy to carry the reference?) that contributes to slowness in perform reference.
What about non-object’s perform?
As an example you progress screenOnClickDoNothing()
out of ViewModel
enjoyable screenOnClickDoNothing() {}
and name it immediately – change viewModel.screenOnClickDoNothing()
with screenOnClickDoNothing()
in lambda.
enjoyable lambdaTest() {
val timeMillis = measureTimeMillis {
repeat(1_000_000_000) {
val display = Display screen {
screenOnClickDoNothing()
}
display.onClick()
}
}
println("lambda1Test: ${timeMillis/1000f} seconds")
}
It takes 0.016 seconds to run.
lambdaTest: 0.016 seconds
What about perform reference? Exchange viewModel::screenOnClickDoNothing
with ::screenOnClickDoNothing
enjoyable globalFunRefTest(){
val timeMillis = measureTimeMillis {
repeat(1_000_000_000) {
val display = Display screen(callback = ::screenOnClickDoNothing)
display.onClick()
}
}
println("funRefTest: ${timeMillis/1000f} seconds")
}
and it takes the identical period of time as lambda.
funRefTest: 0.016 seconds
It makes senses as a result of each have the identical precise decompiled Java code.
public static closing void lambdaTest() {
Display screen display = new Display screen((Function0)null.INSTANCE);
display.onClick();
}
public static closing void funRestTest() {
Display screen display = new Display screen((Function0)null.INSTANCE);
display.onClick();
}
One necessary factor to notice is, though lambda is used, there is no such thing as a new perform reminiscence allotted. Perhaps the compiler is sensible sufficient to optimize it. Thus, it runs quicker than lambda with object’s perform within the earlier examples.
By the way in which, all of the checks I ran up to now is predicated on Kotlin compiler model 1.7.10.
Conclusion
There’s a slight completely different habits between utilizing lambda vs perform reference, however I do not see any sensible use case to make use of both of them. By guessing, I believe lambda ought to cowl 99% of use circumstances as a result of it would not require the item to be initialized first whereas registering the callback.
Right here is the abstract of Lambda vs Operate Reference habits:
Lambda | Operate Reference |
Retains the up-to-date object reference | Retains the copy of the item reference |
Does NOT require the item to be initialized | Requires the item to be initialized |
Calling the perform 1 billions instances might not be sensible, however it does show that perform reference is NOT higher than lambda in Kotlin. Virtually, it in all probability would not matter which one you employ. The distinction is probably going negligible.
Perhaps as a result of I am coming for C++/C# background, I had an impression that perform reference in Kotlin is sort of a perform pointer. You maintain the reference to the perform, and you don’t allocate new reminiscence. No, it isn’t the case in Kotlin.
Given this little or no analysis I did, I’ve determined to make use of lambda over perform reference to implement callbacks as a result of it has much less overhead. It additionally would not scale back readability an excessive amount of, in my view. Simply the additional { }
?
[Updated – Sept 27, 2022]: I take again my phrase. I do discover lambda is a bit much less readable particularly the lambda has arguments whereas engaged on this undertaking.