Gift Effect Quality Tiers: Dynamic Resource Selection Based on Device, Memory, and Network

Learn how to dynamically select live gift asset quality and rendering paths based on device tier, real-time memory pressure, and changing network conditions.

The previous three posts covered runtime optimization for low-end phones, asset size reduction, and iOS integration. One thread runs through all of them: optimizations are static, but device state is dynamic.

The same phone behaves completely differently after two hours of use versus a cold start. The same user has completely different network conditions on WiFi versus a weak cellular signal. Shipping a single set of assets means either wasting visual quality on high-end devices or freezing low-end ones.

This post covers a gift effect quality tiering system: how to dynamically select the right asset version and render path based on device tier, real-time memory pressure, and network state.

1. Device tiers: detect once at startup

20260904155339_12e1ff.png

Split devices into three tiers at launch and reuse the result for the session.

High tier (Snapdragon 8-series / RAM ≥ 8 GB on Android; A15+ on iOS): full 1080p assets, full particle count, H.265.

Mid tier (Snapdragon 7/6-series / RAM 4–6 GB; A13–A14): 720p assets, half particle count.

Low tier (Snapdragon 4-series and below / RAM ≤ 3 GB; A12 and below): 540p assets, entrance and loop segments split, minimum frame count.

On Android, RAM alone is a reliable enough proxy — reading SoC model strings is fragile across the device landscape:

ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);
long totalRamMB = mi.totalMem / 1024 / 1024;
if (totalRamMB >= 6144) return TIER_HIGH;
if (totalRamMB >= 3072) return TIER_MID;
return TIER_LOW;

On iOS, ProcessInfo.processInfo.physicalMemory gives the same signal.

2. Memory pressure: real-time, highest priority

Tier is static; memory pressure is not. A high-end phone can hit a memory warning after hours of use.

Android — listen for onTrimMemory:

@Override
public void onTrimMemory(int level) {
    if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
        GiftQualityController.get().setMemoryPressure(PRESSURE_CRITICAL);
    } else if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
        GiftQualityController.get().setMemoryPressure(PRESSURE_LOW);
    } else {
        GiftQualityController.get().setMemoryPressure(PRESSURE_NORMAL);
    }
}

iOS — listen for the memory warning notification:

NotificationCenter.default.addObserver(
    self,
    selector: #selector(didReceiveMemoryWarning),
    name: UIApplication.didReceiveMemoryWarningNotification,
    object: nil
)
@objc func didReceiveMemoryWarning() {
    GiftQualityController.shared.setMemoryPressure(.critical)
    GiftCacheManager.shared.purgeNonPlaying() // free caches not currently playing
}

When pressure changes, do not interrupt the currently playing effect — mark the downgrade and apply it on the next gift trigger.

3. Network state: affects download strategy, not playback quality

Network state doesn’t affect playback quality of locally cached assets. It determines the download strategy:

  • WiFi: prefetch the next tier’s assets proactively

  • Cellular: load on demand, no prefetch

  • Weak signal (< 1 Mbps): play only what’s already cached, skip network requests

int downKbps = nc.getLinkDownstreamBandwidthKbps();
int networkState = downKbps < 1000 ? NETWORK_WEAK : NETWORK_CELLULAR;

4. Decision matrix: three inputs, one config output

20260904155339_c4c5ad.png

Combine all three dimensions into a single resolver. Callers only ever see a GiftConfig — they don’t reason about tiers or pressure levels directly.

public GiftConfig resolveConfig(int tier, int memPressure, int network) {
    // Memory pressure overrides everything
    if (memPressure == PRESSURE_CRITICAL) return GiftConfig.MINIMAL;
    // Weak or no network: cap at mid, fall further if low-tier device
    if (network == NETWORK_WEAK || network == NETWORK_NONE) {
        return tier == TIER_HIGH ? GiftConfig.MID : GiftConfig.MINIMAL;
    }    // Normal conditions: follow device tier
    switch (tier) {
        case TIER_HIGH: return memPressure == PRESSURE_LOW ? GiftConfig.MID : GiftConfig.HIGH;
        case TIER_MID:  return GiftConfig.MID;
        default:        return GiftConfig.MINIMAL;
    }
}

GiftConfig bundles the asset version, renderer choice, particle density flag, and max frame rate in one object. Nothing else in the codebase needs to know about the decision logic.

5. Three asset versions, server-managed config

Produce three versions of every gift effect at export time:

gift_001_high.svga   // 1080p, full effects
gift_001_mid.svga    // 720p, half particles
gift_001_low.svga    // 540p, entrance + loop split, minimum frame count

Store the URL mapping server-side. The client picks the right URL from GiftConfig without hard-coding version logic:

{
  "id": "gift_001",
  "name": "Aurora Town",
  "assets": {
    "high": "https://cdn.example.com/gifts/gift_001_high.svga",
    "mid":  "https://cdn.example.com/gifts/gift_001_mid.svga",
    "low":  "https://cdn.example.com/gifts/gift_001_low.svga"
  }
}

6. Smooth degradation: don’t interrupt playing effects

20260904155340_b4c62b.png

Hard-cutting quality mid-animation is jarring. Two rules for smooth transitions:

Degrade in the gap between gifts. Let the currently playing effect finish, then apply the new config on the next trigger.

Don’t upgrade immediately either. After memory pressure clears, wait one full playback cycle before stepping back up to a higher tier. Rapid oscillation between configs is worse than staying on the lower one for an extra second.

Wrapping up

The six pieces of the quality tiering system: detect device tier once at startup using RAM as a proxy; monitor memory pressure in real time with highest priority; let network state govern download strategy only; funnel three inputs into one GiftConfig output; manage three asset versions per gift from a server-side config table; and apply tier changes in the gaps between animations.

Together with the previous three posts — runtime optimization, asset reduction, and iOS integration — this completes a full engineering framework for live gift effects.

Next up: gift queuing and concurrency control — how to handle multiple simultaneous gifts, batching, and graceful dropping to keep the stream smooth.

Last updated: