Optimizing On-Demand Resources

Session 221 WWDC 2016

Using On-Demand Resources allows you to create smaller app bundles, enable faster downloads, and add more content than ever before on iOS and tvOS. Learn the recommended approach for delivering apps packaged with On-Demand Resources. Explore strategies for pre-fetching content and understand how to optimize the first-launch experience.

[ Music ]

[ Applause ]

Welcome to Optimizing On-Demand Resources.

I'm Bill Bumgarner with the tvOS Engineering Team.

So in this session, well, in the, in last year's WWDC and in the developer kitchens throughout the year, we've covered really how to use on-demand resources.

In this session, we really want to focus on how you optimize the use of them, and in particular, how you polish the user experience to really make for a fabulous user experience.

So we're going to look at a basic overview of some of the motivations.

How you assign tags, the use of the API's, and then we're going to get into how you optimize that first launch and how you optimize the ongoing user experience as well as we'll look at optimizing app updates, and we will get into some of the implementation details.

So why? Why did we do this on-demand resources?

Well, if we look at a traditional application, it's composed of an application binary and a bunch of resources, and together these make your app bundle.

This is what gets mastered, uploaded to the store, and this is what your customers download and install, and over time, they'll download and install a bunch of applications.

But if we look at the use patterns of these applications, what we notice is that only some of the resources are used a lot.

Some of the resources may have been used once at the tutorial level or something like that, and this ends up eating up a lot of disk space.

And it also means the user has to kind of think about what they want to keep and what they don't want to keep, and we don't really want to make our users into system administrators.

So with an on-demand resources app, what we're really trying to optimize is optimize that resource usage up around what is being used, and make sure it's available before the user actually notices it needs to be downloaded.

So we take your traditional app, and we divide those resources up into the bundled resources and the on-demand resources that are not actually on the system when the app is installed necessarily.

Now, there was some misconceptions in the last year about the size of tvOS applications.

There was a, this notion that they were limited to 200 megabytes.

That's not actually true.

On tvOS, the main app bundle is 200 megabytes, and iOS, it can be up to 4 gigabytes, however, in both cases, they can have up to 20 gigabytes of on-demand resources.

So on-demand resources.

They provide dynamically loaded content that's available on demand or can be downloaded when the app's installed.

It's hosted on the App Store, including hosting across versions.

So upgrades aren't a problem.

Obviously, if you have a user that sticks on an old version, it'll still work.

These are downloadable during application installation.

They're also downloadable during runtime by your request, and you can control the priority with which they're downloaded, and you can shuffle that priority around as the user may change their mind and move around and do whatever they want to do.

As well, all of this works together with the system to provide intelligent content caching as well as intelligent purging.

Again, let's get the user out of the game of administrating their systems.

So the benefits to your app are small or main app bundle, which means it's fast or initial download.

What it means it's a faster period of time between which they click that buy button and they're using your program.

As well, you get a lot richer app content, up to 20 gigs.

I mean, that's a lot of space.

And you can, there can be more apps installed on the system.

They're ready to run, and it reduces the need to manage that storage.

It also means that, you know, they take a bunch of pictures, and some of the on-demand might get flushed out, and that's all automatic.

So how do we do this?

How do we adopt this?

Well, the first thing you have to do is you have to assign tags to all those resources, and you do that by looking within your application, looking at all those resources, and figuring out the roles each resource plays within your app, and when you need them.

And then you go into Xcode, and you assign tags.

Now tags are nothing magical.

They're just strings.

Just any old string you want, Level 1, whatever, and they can be applied to a single asset or a single resource, a sound file, a texture, an image, whatever.

Pure data, or they can be assigned to entire folders.

As well, any given resource can have multiple tags because it might play multiple roles.

So let's go back and let's look at our GreatGame, and let's look at those resources, and, specifically, let's break those resources out by role.

So in this, it's a straightforward level-based application, and it has resources that are required always.

These are the ones that, like, your launch screen, your splash screen, maybe the setting screen.

That kind of thing.

And then it has resources that are in the role of supplying for each level as well as maybe something for a purchasable item or an in-app purchase.

And to tag these, it's pretty straightforward.

Just give them the same name as their role.

So when we do these, when we look at these, what's the strategy for tagging these things?

Well, only put in the main bundle the resources that are absolutely positively needed by the application all the time.

Your loading screen.

Your splash screen.

That kind of thing.

And then you apply tags to everything else that's there.

And each tag can be applied to up to 512 megabyte of assets or resources.

However, we really recommend that you stick around that 64-megabyte limit simply because that makes the downloads that much faster and less perceptible to the user.

And, again, you can have more than one tag per resource, and it, whenever you use one of the tags, it will pull down all the resources as appropriate.

So now that we've got everything organized and tagged, let's look at the runtime side.

Within the runtime, we tried to make the API very simple, and, in fact, it's only one class.

There's the bundle resource request class.

Now you create an instance of this to manage all the access to your on-demand resources.

It's created with a tag or a set of tags, and it has some other options for managing it.

You use it to begin and end accessing to those resources.

Begin accessing is what triggers a download if necessary, and end accessing is what tells the system, hey, I'm done.

And on this object, you can also set the priority.

If you have a particularly large download or a particularly slow connection, you can track progress, and there's also the possibility of an error, which we'll talk about in a minute.

One of the interesting things about this class is each instance is one shot.

They are very lightweight, very cheap to create.

So that means when you create one, and you call begin accessing on it after you've called end accessing, that object's done.

Create a new one.

And one concept that we find is very important to take to heart is that the request is decoupled from access.

So you decouple when you make that request from when you're going to use the resource, and we can, we'll cover this in the predictive loading.

So we can predict what the user's going to do to make sure they never see those loading screens.

So looking at the actual code, it's really straightforward to initialize a bundle resource request.

Just give it a set of tags, and you have your request.

If you want to begin the accessing those tags, you call begin accessing, and it has a completion handler, and that completion handler will be called with an error if there is an error, or it will be called no error, and you're resources are available.

To get at the resources, you use the bundle API.

So you grab the NS bundle instance, I'm sorry, the bundle instance [inaudible] renaming, from the request, and you just use the normal resource request methods on bundle to get a hold of that.

And once you're done, it's very important to call end accessing.

This tells the system that you're done with that resource.

Now it's very important to note that that doesn't mean the system's actually going to delete the resource.

Our systems are very lazy.

They don't want to do any extra work, and deleting stuff's extra work.

So when you're loading resources, you can control the priority.

Like, say you're moving through your, a game, and, and the users change their mind, and you were downloading this level over here, now you need to download Level 5.

Well, you start at the begin access.

You can go, and you can change the loading priority to bump the priority on the Level 5 stuff and decrease the priority on Level 3 if you think they might go back, or you can end it.

It's just a value from zero to one, but there is the special urgent priority.

There will be times when the player has decided to go off in a direction that you couldn't possibly have predicted, and you need to just download everything right now.

And in this case, this special high urgency loading priority can be used.

It suspends all other download, and it also maximizes the throughput.

So there's no network throttling, and it also maximizes CPU usage dedicated to that download.

Finally, there's conditional requests.

Now a conditional request can be used to check to see if the resources has already been downloaded.

So if you remember when I said end accessing doesn't necessarily delete resources, well, the player's been playing a game.

They've gone through Level 1, 2, and 3.

You've ended accessing to Level 1, 2, and 3.

They quit the game.

They've gone off and done something else.

They come back, relaunch the game, and they want to replay Level 1.

Well, you can use conditionally at, or say they want to select between levels.

You can use conditionally begin accessing to check what levels are already downloaded, and give them indication of what's already available to play.

Or if they dive into a level, and you've broken up your resources by role within the level, maybe you optimize the first part of the level to only showing the trees and bushes and enemies that happen to be on disk at that time while you download the rest of it in the background.

So all of this is about you being able to avoid loading screens whenever possible.

And if the items are already downloaded, this works exactly like begin accessing.

And as well, as always, call end accessing, even if you got the callback, and it was false, and you decided you didn't want to trigger it download, always call end accessing.

So now you have a working application, but let's look at that first launch.

And let's look at a timeline in particular, and we're going to go with a timeline from the moment the user buys the application in the store, it gets downloaded, it's installed, and then the first launch happens, and what do we do.

The first thing we do, we begin accessing Level 1, which triggers a download, and then the player can play the game, and then they get to Level 2, and we do begin accessing, and it downloads, and they wait.

They play, and we keep doing this.

Level 3, download, wait, play.

And even with the purchasable items, in-app purchases.

Download, wait, play.

That's not a good user experience.

Making the user constantly look at loading screens, no.

We're not going to do that.

So the first thing we're going to do is we're going to take advantage of features and on-demand resources that are built in to optimize that first launch from the get go, and the first thing we're going to use is initial install tags.

And the next thing we'll use are the pre-fetch tags.

And with these in place, then that Level 1 will be downloaded and installed when the app is purchased, and Level 2 will be downloaded and installed immediately after, and hopefully the user can dive in and just start playing.

And then we'll look at predictive download, but first let's take a step and look at how we can configure the initial pre-fetch.

So initial install tags.

These are tags that are marked up in Xcode to be downloaded as a part of your application installation.

You can have up to 2 gigabytes with these resources, which is a lot.

It's part of the size shown in the App Store, and, in fact, when the little download progress indicator goes, that reflects the initial install tags as well.

The pre-fetch tags, they're a little bit different, but you can have as many pre-fetch that's as you want up to 4 gigabytes minus the size of the initial install.

And it follows an order specified in Xcode, and the pre-fetch tags are downloaded immediately after the initial, and they don't prevent app launch.

So the user will be able to dive into the game and start playing even though the pre-fetch stuff's coming down in the background.

And in Xcode, this user interface looks like this.

This is the resource tags inspector inside of your target editor for your application.

It's got three sections: The initial, the pre-fetch, and the download only on demand tags.

You move stuff in the initial.

These are the ones that will be bundled with your app and installed at the same time.

The pre-fetch, these will be downloaded after in the order you see on the screen, and then, finally, the download only on demand are the ones who will only be downloaded when you begin accessing on those tags.

So going back to our timeline.

We talked about predictive loading very briefly, but what does that really mean?

Well, we got our initial.

We got our pre-fetched, and we're still making the user wait at Level 3.

So, instead, if we simply begin accessing the Level 3 tags somewhere in Level 1 or 2, it'll probably be downloaded and ready to play by the time the player gets to there.

As well with purchasable items, if you have a particular point in your app, game, or whatever, that you think it's likely or you hope the player's going to go and do an in-app purchase, go ahead and begin accessing there.

You don't give them access until you got the receipt, but at least it'll be there, and there'll be no waiting.

Now we've got one big green timeline and a very happy user.

We've talked about this level-based game, which is a very linear access pattern.

It's very convenient for making beautiful slides.

It's not the real world.

In a linear access pattern, the majority of the assets are going to be used.

They're very much going to be used in order.

Your tag size isn't really that critical because you can always stay well ahead of the user in terms of getting accessing, but the issue is that, of course, nothing's ever linear.

And in particular, a lot of times we'll have an app that has a very random access pattern, and the player may go all over the place, or there may be things that are shared amongst levels, or they, you know, they may select certain configurations, or they may buy certain in-app purchases, and in this case, the goal is really to try to predict as much as possible to try to pull down stuff before the user needs it, but in the case where you have to pull it down really on demand at that moment, stick to small tag groups, and that will make very fast downloads.

And you can download sets of tags proactively.

It's OK to go and kind of just guess at what's going to be needed and let them be, put down on disk because, of course, we have this intelligent caching mechanism that's working in the background to make sure that the right things get deleted if there is disk pressure.

And, again, end accessing doesn't mean deletion.

So if you go off, and you predictively download a bunch of stuff, and then you don't ever need it, well, that's OK.

Just end accessing, and it'll probably still be there when you do need it whenever in the future.

Now there's another pattern, which is kind of in between, and that's your explorative access pattern, and this is the one where it's, you know, the, that you wonder from village to village on quests and things like that.

And in this case, there is limited prediction.

Many possibilities will not be used, but you often are at a branch, and when you're at the branch, you can load a subset of your tags.

Oh, the user make a left, make a right.

So I'll load the left tag and the right tag, and then as soon as the user makes the decision, expresses their intent to go right into accessing on left, let that download stop, focus on the right, and start predicting a one step ahead.

So now we have this working app.

We've got a great user experience, and that's all well and good, but let's look at some of the implementation details that are going on behind the scenes that you can be aware of to optimize this experience even further.

And in particular, as I said, the app bundles, they're limited to 4 gigs on iOS, 200 megabytes on tvOS, but you can have up to 20 gigabytes of on-demand resources, and of those, up to 2 gigs will be downloaded and installed with the app, and up to 4 gigs will be pre-fetched minus those 2 gigs or up to 2 gigs of install.

There's some additional numbers to keep in mind.

You can have up to 2 gigabytes of resources active at any one time.

So you go and you begin accessing on up to 2 gigabytes of tags, and those will be downloaded, and they'll be made available, and that's great.

When you go over the 2 gigabytes, what happens is that the begin accessing method, that callback gets an NS error that indicates that you are out of tagged resource space.

You need to go in to end accessing on some current set of tags to free up some space to allow more to be accessed.

Now, reiterating this point because there was another point of confusion, if you have 2 gigs of tags that are pinned down, and you want to access more, and you go into accessing on, on 500 meg of them, and pin another 250, that 500 meg of resources are probably not going to be deleted.

They'll be around and available, but it just lets the system know if things get dire, it can go and clean them up.

Any one tag, again, up to 512 meg, try to stick to 64 megs or lower.

And you can have up to 1,000 total asset packs.

What the heck's an asset pack?

Haven't mentioned that word yet at all.

Well, an asset pack is fallout from the Xcode build system.

It's the way your application is built and mastered.

It's the way the on-demand resources are compiled together and managed by the store.

If we look at our great game, in this case, a role-playing game, it doesn't matter.

We have our tags, and as is very typical, resources tend to be used more than once.

Things get used from Level 1 to Level 2.

Enemies become friends.

That kind of thing.

So we have these two resources here that then get used on Level 2.

So they're tagged with Level 1 tags and Level 2 tags.

So our tag set looks like this.

We've got four resources with one tag and two resources with two tags, and while we only have four tags, this ends up with six asset packs.

Now if you think through like a random access game, this could be a stumbling block.

If you have a lot resources that are shared across lots of different roles such that many of those resources have five, 10, 15 tags, then the cross product of all those could end up exceeding that 1,000 tag or 1,000 asset pack limit, and that is something to be aware of.

So in the life cycle of any game or any application, of course, you're going to have application updates.

You want to improve that user experience, get the users coming back.

And on-demand resources have been optimized for application updates as well.

A little bit, maybe a little surprising, but if you think it through, it makes sense.

In particular, we start out with our version 1.0 game, and we have a bunch of resources in that game, bunch of tag ones and some main bundle ones, and when we ship version 2, well, we've modified something in the main bundle.

We may have added some resources to Level 1, modified a couple things on Level 2.

We added a whole new level, and that's all well and good.

So what happens across the updates?

So the first thing is when you update resources, update tagged resources, nothing is redownloaded automatically.

It's redownloaded when it's first accessed.

We don't want to redownload the tutorial level when the user's way beyond that.

Any unchanged resources will just stay on disk, and they can be accessed without download.

New resources, they'll be downloaded when accessed.

So, really, once again, it's the system taking a very lazy approach to this.

In this case, because we can't predict what changes you've made to the app that are going to be required by whatever state the player or the user is left your app in, we're going to leave it up to you to trigger the begin accessing to trigger the updates and pull down the new stuff.

And, in fact, on first launch, you might want to go and begin accessing a couple of your changed things to make sure that they are made available before the user notices.

So best practices for this.

Just avoid making unnecessary modifications to tagged resources, including things, like, you know, we had a situation where somebody made a spelling change and was surprised when everything got redownloaded the first time they accessed it.

If you change one resource in an asset pack, it's going to trigger the download of the whole asset pack, and this is a, just an implementation detail.

So keep that in mind.

So what you can do instead is, like, say, for example, in the case where we added a couple of extra resources to Level 1, we can make Level 1 update one tag, and then begin accessing on both of those, pull them down, and when they're both available, then allow the user to play Level 1.

Keep those tags consistent, and from the beginning you really want to design with a separation of updateable content versus static content in mind.

And all that means is, you know, where you may have a tag for one single role in your application, maybe you want to divide that tag into multiple tags where it's something that you know will probably never, ever change and a handful that will.

Now how does all this contribute to the intelligent content caching?

So on tvOS, one of the goals of the OS is to never have the user be aware of this usage.

Never have to go and delete anything.

Never even have to think about it.

And as a part of that, there's this whole cache management system and automatic purging system.

And the system's going to purge resources from the disk when there's a dire need for disk space, and there's multiple levels to it, and it starts out the lowest priority, and we'll clean up caches and whatever, and then it'll move up to higher and higher priority.

And so, again, pounding on this point, it's important to ending access to a resource when you're done with it, and that does not mean deletion.

There's a number of variables that inform the system about purge order.

Obviously, least recently used go away first, to a degree.

You also have control over the preservation priority.

This preservation priority can be a sign to the bundled resource request, and it's a number from zero to one that just determines when, what order the system will delete stuff.

It's isolated to your application.

So there's no cheating.

It doesn't help you to set everything to one.

It just means we're going to blow everything away if it gets that bad.

And if your application's running, it's going to be the last one the system tries to purge resources from.

And this is very important.

Don't use temp or caches.

I mean, obviously use them if you need some temporary stuff or, you know, per session cache, but because we can't know anything about the structure of the data in a temporary cache, the system's going to treat those are purgable at a pretty low priority.

They're going to be purged first, and when they're purged, they're purged in their entirety.

So, finally, in conclusion, use on-demand resources.

In particular on tvOS, the use of on-demand resources really provides for a much more optimal user experience.

It leverages that always on network connection.

On iOS, things can be a little bit trickier, but there's a number of ways that it can be used very successfully.

It will give you that smaller app bundle, which gives you a much faster customer acquirement time from store to your customer playing your game using your program doing whatever with your content.

You get this richer app content.

You now have 20 gigabytes of space to play with to put together whatever you want.

And it also means for the user, they don't have to think about it.

They can just install as many applications as they want.

They don't have a barrier to entry there, and they don't have to think about the storage.

So that's on-demand resources optimization.

For more information, there's a whole web page devoted to this.

There was also a number of related sessions earlier in the week.

Encourage you to review them.

Thank you.

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US