Reaper: The Open-Source SDK for Eliminating Dead Code in iOS and Android
Introducing Reaper, the open-source SDK designed to effortlessly find and delete unused code in iOS and Android applications. Leverage runtime analysis, proven by companies like Duolingo, to enhance app performance, reduce bugs, and streamline your mobile development process by keeping your codebase clean.

The journey of writing code has become remarkably fluid. Our goal is to make the process of deleting code equally effortless. We're proud to introduce Reaper, now an open-source SDK for both iOS and Android.
Originally an Emerge Tools product, Reaper gained recognition for helping companies like Duolingo significantly trim their iOS codebases, with one instance resulting in a 1% reduction. Following the footsteps of Emerge Tools’ Launch Booster, we are now open-sourcing Reaper, making its power accessible to all developers.
This post will delve into what Reaper is, underscore the critical importance of identifying and removing dead code, and explain how Reaper operates across both mobile platforms.
How Reaper Works: Runtime Analysis
Static analysis involves inspecting code without executing it to uncover bugs, enforce coding standards, or gain insights. Compilers, for instance, can analyze a project and strip out unused code. Tools like Periphery leverage static analysis to pinpoint dead code.
While static analysis is highly effective, it has limitations. A compiler might not detect a stale feature flag if there are strong references to it, even if the feature is no longer active in production.
Reaper takes a different approach, finding dead code through runtime analysis. It diligently monitors how users interact with your application in real-time to identify code paths that are never executed. Despite OS-specific nuances, Reaper’s core principle remains consistent across iOS and Android: let real user behavior guide you in determining which code can be safely removed.
The Reaper SDK automatically detects which code is triggered during a user's session. This collected data can then be aggregated by application version, providing a clear overview of types that have not been executed.
Example report as shown in a Duolingo case study
Furthermore, this data can be compared across multiple versions of your app. Code that remains untouched across several versions is a strong candidate for deletion.

Why Deleting Code is Important
Some developers humorously state that "all code is bad code". While this sentiment isn't meant to be taken as absolute dogma, there's compelling evidence regarding the downstream impacts of increasing lines of code. More code typically leads to:
- Increased security vulnerabilities
- More bugs
- Slower compilation times
- Extended build pipeline durations
- Greater complexity and difficulty in maintenance
Reaper for iOS
Reaper for iOS is now available as an open-source Swift package. As part of this transition, the API has been updated to enable custom handling of detected types, allowing you to upload this data to your own server infrastructure.
Using Reaper involves a two-step process:
- First, you execute a script that statically analyzes your app's binary to generate a comprehensive set of all types that Reaper is capable of tracking.
- Next, you aggregate usage reports from your production users. The difference between these two sets reveals the types that were never utilized in production and can, therefore, be safely removed from your codebase.
How it works under the hood
At its core, Reaper for iOS functions by inspecting the Objective-C and Swift runtimes for metadata already employed to track the lifecycle of types. This ingenious method ensures zero overhead during your app's runtime, as it only inspects pre-existing fields at the data upload stage.
For Objective-C classes, Reaper utilizes the RW_INITIALIZED bit, which the runtime sets the first time a class is accessed. This flag is precisely how the runtime knows to invoke +(initialize) only once per class. The check for this flag boils down to these three lines:
objc_class *metaClass = (__bridge objc_class *)object_getClass((__bridge id)classPtr);
class_rw_t *writableClassData = metaClass->bits.data();
BOOL isInitialized = !!(writableClassData->flags & (1 << 29) /* RW_INITIALIZED */);
The process is more intricate for Swift types. While some Swift types also set the RW_INITIALIZED bit, others completely bypass the Objective-C runtime. However, even those that bypass the ObjC runtime can often still be tracked due to other flags used by the Swift runtime. When you run our provided scripts to determine which types Reaper supports, the Swift binary metadata is meticulously inspected to identify which types will be reliably trackable at runtime.
Reaper for Android
The Android Reaper SDK has always been open source, but it now offers enhanced flexibility, allowing you to integrate it with your own custom backend. This is achieved by simply editing the Android manifest for your application:
<meta-data
android:name="com.emergetools.OVERRIDE_BASE_URL"
android:value="https://example.com/foo" />
With this configuration, your app will transmit reports to https://example.com/foo/report and any Reaper-related errors to https://example.com/foo/reaper/error. An example commit demonstrating this integration using our demo app, Hacker News, is available for reference.
Similar to its iOS counterpart, using Reaper on Android involves subtracting the set of classes observed in production from those identified at build time to pinpoint unused classes.
How it works under the hood
In contrast to iOS, the Android environment does not provide direct access to runtime metadata that tracks the lifecycle of a type. Instead, we manually instrument classes at build time. This instrumentation is seamlessly integrated into the Emerge Tools Gradle plugin.
The Java Virtual Machine (JVM) operates on bytecode 'classes'. While classes in Java and Kotlin directly correspond to JVM classes, many higher-level language constructs (such as lambdas, inner classes, and continuations) also generate unique JVM classes. Here's a typical prefix of an Android class in the .smali format:
.class public final Lcom/emergetools/hackernews/HNApplication;
.super Landroid/app/Application;
.source "SourceFile"
.method public static constructor <clinit>()V
.registers 2
return-void
.end method
.method public constructor <init>()V
.registers 3
invoke-direct {p0}, Landroid/app/Application;-><init>()V
return-void
.end method
# virtual methods
.method public final onCreate()V
.registers 3
invoke-super {p0}, Landroid/app/Application;->onCreate()V
[.. .snip…]
This snippet highlights two 'magic' methods: <clinit> and <init>.
<clinit>: This is the static initializer, invoked once for each class before that class is ever used.<init>: This is the class initializer, called every time an instance of the class is created.
For most JVM classes, at build time, our process involves:
- Computing the SHA256 hash of the class signature and extracting the top 64 bits.
- Then, for both
<clinit>and<init>methods (if present), we inject a call tologMethodEntry, passing the extracted top bits from the hash.
After this instrumentation is applied, the .smali code appears as follows:
.class public final Lcom/emergetools/hackernews/HNApplication;
.super Landroid/app/Application;
.source "SourceFile"
.method public static constructor <clinit>()V
.registers 2
sget-object v0, Lcom/emergetools/reaper/ReaperInternal;->INSTANCE:Lcom/emergetools/reaper/ReaperInternal;
const-wide v0, 0x3203966e22ecd0dcL
invoke-static {v0, v1}, Lcom/emergetools/reaper/ReaperInternal;->logMethodEntry(J)V
return-void
.end method
.method public constructor <init>()V
.registers 3
sget-object v0, Lcom/emergetools/reaper/ReaperInternal;->INSTANCE:Lcom/emergetools/reaper/ReaperInternal;
const-wide v0, 0x3203966e22ecd0dcL
invoke-static {v0, v1}, Lcom/emergetools/reaper/ReaperInternal;->logMethodEntry(J)V
invoke-direct {p0}, Landroid/app/Application;-><init>()V
return-void
.end method
# virtual methods
.method public final onCreate()V
.registers 3
invoke-super {p0}, Landroid/app/Application;->onCreate()V
[.. .snip…]
At runtime, if Reaper is enabled, these hashes are inserted into a thread-local cache, periodically consolidated into a set for the entire application, and finally, at the end of a user session, queued to be reported to the server via a worker job.
To prevent performance issues and reentrancy problems, we deliberately avoid instrumenting a number of core libraries and the Reaper SDK itself. Furthermore, we do not add a <clinit> or <init> method if one is not already present in the class, meaning most interfaces or abstract classes are not instrumented. In large applications, which commonly contain 50-100k classes, approximately 85% typically possess either a <clinit> method, an <init> method, or both.
In our sample repository, we've provided:
- A script for extracting instrumented classes and hashes from
.aabfiles. - A demo backend designed for receiving reports.
The script can be utilized as follows:
git clone https://github.com/getsentry/reaper-server.git
cd reaper-server
./reaper.py myapp.aab -o reaper.tsv
Concluding Thoughts
Numerous reports suggest that generative AI is accelerating developer velocity. Google Dora’s "Impact of Generative AI in Software Development" highlights productivity improvements but also points out a "negative impact on delivery stability" with AI adoption. Similarly, GitClear's AI Copilot Code Quality report notes a higher-than-expected rate of code additions alongside a significant increase in code duplication.
This is not to diminish the capabilities of generative AI (as demonstrated by tools like Seer). Tools for writing code are advancing daily, making it crucial that tools for monitoring and maintaining code keep pace. We sincerely hope that open-sourcing Reaper empowers teams to make substantial progress in managing their iOS and Android codebases. We'll leave you with this timeless piece of software wisdom:

Share on Twitter Share on Bluesky Share on HackerNews Share on LinkedIn