Video first

If you want to watch the video, you can go to Bilibili or YouTube. Not convenient to see the video, the following article.

Kotlin has a handy keyword called inline that will help you inline a marked function. Inline means that the called function is embedded in the code at compile time:

The benefit is obvious, the call stack gets shallower, right?

But the effect of this call stack optimization is actually so small that it should be ignored. Should be ignored, not should be ignored, because not only is this kind of optimization useless, but it can also become negative because of the bytecode bloat generated by the compilation due to multiple copies of the code. So, what do we need something like this for?

– ah? What am I talking about?

The text is here

Hi, I’m Zhu Kai from throwline.

In Java, there is a concept called compile-time Constant, which intuitively means that the value of this variable is fixed and can be determined by the compiler at Compile time. In terms of code, the variable needs to be final, the type can only be a string or primitive type, and the variable needs to be assigned at declaration time. The right-hand side of the equals sign can’t be too complicated. The bottom line is that you want the compiler to be able to see it at a glance. This compile-time constant is compiled inline by the compiler, which simply replaces the variable name at the call with your value. This simplifies the program structure and makes it easier for the compiler and JVM to make various optimizations. That’s what compile-time constants do.

Compile-time constants have a special keyword in Kotlin called const: A variable that begins with const val is treated as a compile-time constant by the compiler for inline compilation:

— If you don’t match the compile-time constants, you’ll get an error.

inline

Inline variables with const; In addition to variables, Kotlin has added support for inlining functions. In Kotlin, you add the inline keyword to a function and the function is compiled inline. But!!! Although inline, the purpose of the inline keyword is completely different from that of const.

Why are compile-time constants so restrictive? Because only by meeting these limitations can the compiler and JVM be able to do the optimizations that make this inline operation meaningful. If it gets a little more complicated, it won’t be optimized. I don’t know what it means to be a little bit complicated, but the inlining of a function, it’s pretty complicated, it’s pretty immutable. In fact, the inlining of functions does result in a passive optimization. As I mentioned earlier, if a function is removed, the performance loss will be reduced, but the performance loss caused by the call stack itself is very small, and the optimization is almost no optimization at all. This fact may not be intuitive, but think about it this way: In all the performance optimization specifications we’ve seen, have you ever seen an optimization strategy like “write fewer methods to reduce the call stack”? Didn’t you? Why is that? Because this optimization doesn’t make sense. At the same time, function inlining differs from constant inlining in that the function body is usually much more complex than constant inlining, which causes the function body to be copied to every call. If the function body is large and many calls are made, the compiled bytecode will be much larger. We all know that compression of compilation results is a major indicator of application optimization, and that function inlining is obviously detrimental to this indicator. So inline for performance optimization? It doesn’t exist.

So the question is: what does inline do?

In fact, the inline keyword can inline not only its own internal code, but also its own internal code. What do you mean “inside of the inside”? Is an argument to its own function type.

For example, I changed the definition of the hello() function to something like this, adding an argument of function type:

Accordingly, this parameter needs to be filled in at the call.

I can write it as an anonymous function:

Or, to make it simpler, write it as a Lambda expression:

Because Java does not have native support for function-type variables, Kotlin needed to find a way to implement his new concept in the JVM. And what did it do? The idea is to use a JVM object as the actual vector for a variable of function type and let the object execute the actual code. That is, after the changes I made to the code, every time hello() is called, the program creates an object to execute the code in the Lambda expression. Although this object is used and then immediately discarded, it is created.

What’s the harm in that? In general, there’s no harm in creating more objects. But imagine if this function was executed in a loop:

Is the memory footprint going up all of a sudden? And the point is that you, as the creator of a function, do not know and cannot dictate where the function is called, which means that it is out of your control whether or not the function appears in high frequency scenarios such as loops or interface refreshes. In this way… All of these functions have performance implications. Higher-order functions are a convenient feature of Kotlin compared to Java, but have a performance pitfall that… How can you trust it?

This is where the inline keyword comes in.

Inline inline inline inline inline inline inline inline inline inline inline inline inline inline inline inline It also inlines the arguments to its internal function types — those Lambda expressions. In other words, the function is posted fully expanded by the compiler:

Does this optimization avoid creating temporary objects for function type arguments? In this case, are they not afraid to call them in high frequency scenarios such as loops or interface refreshes?

This is where the inline keyword comes in: higher-order Functions have their natural performance defects, so we use the inline keyword to make Functions compile inline to reduce the creation of parameter objects and avoid performance problems.

So, is inline used for optimization? Yes, but you can’t mindlessly use it, you need to make sure it leads to optimization before you use it, otherwise it might become negative optimization. Think about it another way: since inline is an optimization, why didn’t Kotlin turn it on directly instead of making it an option, and it’s off by default? Just because it’s not really optimization, it’s up to us to decide whether to add it or not. So how do you make that judgment? Simple, if you’re writing a higher-order function, you’ll have parameters of the function type, inline.

HMM… However, if your team has extreme package size requirements, you can also use inline as appropriate, such as strict code that uses inline only for higher-order functions that are called frequently – this can be a bit difficult to implement, but generally speaking, the principles I just described will suffice.

In addition, Kotlin’s official source code has an alternative use of inline: to call a Java static method directly in a function:

A sneaky way to remove the prefix from these Java static methods to make calling easier:

It is important to note that this usage is not inline as it was originally created, nor is it inline at its core. It is an alternative to inline. This is fine because the function body is compact and does not cause bytecode bloat. You can use this if you have a similar situation.

At this point… You know how to use inline functions? Let’s just… A little bit further?

noinline

After inline, let’s talk about another keyword: noinline. Noinline means straightforward: inline is inline, and noinline is not inline. For an inline function marked inline, you can add the noinline keyword to any or more of the function types’ arguments:

When added, this parameter does not participate in inline:

Easy to understand? That’s easy to understand. (frowns) But what’s the point? Why turn this optimization off?

The first thing we need to know is that the argument to a function type is essentially an object. We can call this object as a function, which is the most common use:

But we can also use it as an object. For example, consider it as a return value:

But when a function is inlined, its internal parameters are no longer objects, because they are taken to the call by the compiler to expand. That is, when your function is called like this:

The code will compile like this:

Ah? Who are you calling, please?

See the problem? When a function is inlined, its arguments to function types are no longer objects because their shells are removed. In other words, to the compiled bytecode, the object doesn’t exist at all. How do you use an object that doesn’t exist?

So when you try to use one of these arguments as an object, Android Studio will report an error telling you that it can’t compile:

That…… What if I really need to use this object? Add noinline:

When noinline is added, this parameter does not participate in inline:

Then we can use it normally, too.

So, what does noinline do? Is used to locally and directionally turn off inline optimization of functions. If it’s optimization, why turn it off? This kind of optimization reduces Kotlin’s functionality to a certain extent because the parameters of the function type in the function cannot be used as objects. When you need this feature, turn it off manually. This is another reason that inline is off by default and needs to be turned on manually: it Narrows Kotlin’s functionality.

So how can we tell when to use noinline? Simple, even simpler than inline: You don’t have to judge, Android Studio will tell you. When you use a dirty operation on a function type parameter in an inline function and Android Studio refuses to compile, add noinline.

crossinline

Finally, crossinline. This is an interesting keyword. Noinline is a local closed inline optimization, right? And this crossinline, it’s locally enhanced inline optimization.

Let’s look at the code first. There is an inline function and a call to it:

Suppose I add a return to the Lambda expression:

Which function does this return end? Is it hello() on the outside or main() on the outside?

By the usual rules, it must end hello(), right? Since hello() is close to it, return must end withdirectlyThe function that wraps it around. But think about it. Hello () is an inline function, right? What happens to inline functions after compilation and optimization? It’s going to be paved, isn’t it? This call, when paved, looks like this:

What function does return end? It’s the outer one, right? That is, for an inline function, the return of the Lambda in its argument terminates not the inline function, but the function that calls the outer layer of the inline function. This is the truth!

That’s the way it works, but there’s a problem. What is it? When I return a function, it depends on whether the function is inline. Wouldn’t I have to drill into the function to see if there was an inline keyword to see how my code would behave? That’s too hard!

This inconsistency can be so frustrating that Kotlin made it a rule not to use returns in Lambda expressions, ** unless — ** the Lambda is an argument to an inline function.

Then the rules are simple:

  1. Return in Lambda terminates not the immediate outer function, but the outer function and the outer function;
  2. But only Lambda arguments to inline functions can use return.

Note: Lambda can explicitly specify the return location with return@label, but that is not today’s topic.

This eliminates ambiguity and avoids the hassle of having to double-check each function to see if it is inline.

But… If we make things a little more complicated — for the last time, they can’t be any more complicated:

This time, I put the parameter on the main thread with runOnUiThread(), which is a very common operation.

However, this creates a problem: The return on the last line of the call terminates its main() function, but since it is placed inside runOnUiThread(), the hello() call to it becomes an indirect call. The indirect call is, in plain English, cut off from the outer hello() function. The connection with hello() is severed, and the outer main() is out of reach, meaning that the return in the Lambda cannot terminate the outer main() function.

What does that mean? When Lambda arguments to an inline function are called indirectly from within the function, the return in the Lambda will not behave as expected.

This is serious because it creates a problem with the stability of the Kotlin language. The results are unpredictable. Does that work, right?

So what?

Again, Kotlin’s choice is a one-size-matic one: arguments of function types in inline functions do not allow such indirect calls.

To solve! If I can’t solve the problem, I’ll solve the problem.

What if I really have a need? What if I really need to call indirectly? Use crossinline.

Crossinline is also a keyword used on arguments. When you add crossinline to an argument that needs to be called indirectly, you remove the restriction from it and it can be called indirectly:

This, however, leads to the same “inconsistencies” as before, for example if I add a return to the Lambda:

image-20200904031015909

Who does it end with? RunOnUiThread () around it, or is it still the outermost main()?

To address this discrepancy, Kotlin has added an additional rule: arguments of the type of function modified by Crossinline in inline functions will no longer have the benefit of “Lambda expressions can use return”. So the return does not face the question of “who to end”, but simply does not write it.

That is, you can only choose between an indirect call and a Lambda return.

What if I want both? — I can’t help it. I really can’t.

So when do you need crossinline? When you need to break the “can’t call arguments indirectly” limit of inline functions. But as with Noinline, you don’t have to tell for yourself, just add it when you see an error from Android Studio.

conclusion

Inline, noinline, and Crossinline are three useful and handy keywords that we see in Kotlin’s official source code as well as in various open source libraries.

By now, their meanings and uses have been explained, and the process is complicated, but the conclusion is simple. To sum it up:

  1. Inline lets you optimize the structure of your code by inlining — that is, inserting function content directly into the call — to reduce the creation of function-type objects.
  2. Noinline partially turns off this optimization to get rid of the inline restriction that “you can’t use a parameter of a function type as an object”;
  3. Crossinline is a local enhancement of this optimization, allowing arguments of function types in inline functions to be used as objects.

That’s it for this episode. If it helped, don’t forget to like, retweet and follow. If you want to learn more from me, welcome to join my knowledge planet and sign up for my Android advanced serialization course, very valuable, very valuable! Scan the code and my little assistant can get the details. I’m a throwline. I don’t compete with you. I just help you grow. See you next time!

image.png