Skip to content

Commit 7b43986

Browse files
fix(YouTube): Improve litho filtering performance (#4904)
1 parent 7a245d3 commit 7b43986

File tree

4 files changed

+150
-77
lines changed

4 files changed

+150
-77
lines changed

patches/api/patches.api

+1
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,7 @@ public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPat
15341534

15351535
public final class app/revanced/util/BytecodeUtilsKt {
15361536
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
1537+
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;[Lapp/revanced/patcher/util/smali/ExternalLabel;)V
15371538
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z
15381539
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z
15391540
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z

patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ internal val readComponentIdentifierFingerprint = fingerprint {
4545
strings("Number of bits must be positive")
4646
}
4747

48+
internal val elementConfigFingerprint = fingerprint {
49+
strings(" enableDroppedFrameLogging", " elementDepthInTree")
50+
}
51+
4852
internal val emptyComponentFingerprint = fingerprint {
4953
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
5054
parameters()

patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt

+122-76
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@ package app.revanced.patches.youtube.misc.litho.filter
44

55
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
66
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
7-
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
87
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
98
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
109
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
10+
import app.revanced.patcher.patch.PatchException
1111
import app.revanced.patcher.patch.bytecodePatch
12-
import app.revanced.patcher.util.smali.ExternalLabel
12+
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
13+
import app.revanced.patches.youtube.layout.returnyoutubedislike.conversionContextFingerprint
1314
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
1415
import app.revanced.patches.youtube.misc.playservice.is_19_18_or_greater
1516
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
1617
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
1718
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
19+
import app.revanced.util.addInstructionsAtControlFlowLabel
1820
import app.revanced.util.findFreeRegister
21+
import app.revanced.util.findInstructionIndicesReversedOrThrow
1922
import app.revanced.util.getReference
2023
import app.revanced.util.indexOfFirstInstructionOrThrow
24+
import app.revanced.util.indexOfFirstInstructionReversed
2125
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
2226
import com.android.tools.smali.dexlib2.AccessFlags
2327
import com.android.tools.smali.dexlib2.Opcode
28+
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
2429
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
25-
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
30+
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
2631
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
2732
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
33+
import com.android.tools.smali.dexlib2.immutable.ImmutableField
2834

2935
lateinit var addLithoFilter: (String) -> Unit
3036
private set
@@ -53,42 +59,48 @@ val lithoFilterPatch = bytecodePatch(
5359
* The buffer is a large byte array that represents the component tree.
5460
* This byte array is searched for strings that indicate the current component.
5561
*
56-
* The following pseudocode shows how the patch works:
62+
* All modifications done here must allow all the original code to still execute
63+
* even when filtering, otherwise memory leaks or poor app performance may occur.
64+
*
65+
* The following pseudocode shows how this patch works:
5766
*
5867
* class SomeOtherClass {
59-
* // Called before ComponentContextParser.parseBytesToComponentContext method.
68+
* // Called before ComponentContextParser.readComponentIdentifier(...) method.
6069
* public void someOtherMethod(ByteBuffer byteBuffer) {
6170
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
6271
* ...
6372
* }
6473
* }
6574
*
66-
* When patching 19.17 and earlier:
75+
* When patching 19.16:
6776
*
6877
* class ComponentContextParser {
69-
* public ComponentContext ReadComponentIdentifierFingerprint(...) {
78+
* public Component readComponentIdentifier(...) {
7079
* ...
71-
* if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
80+
* if (extensionClass.filter(identifier, pathBuilder)) { // Inserted by this patch.
7281
* return emptyComponent;
73-
* ...
82+
* }
83+
* return originalUnpatchedComponent;
7484
* }
7585
* }
7686
*
7787
* When patching 19.18 and later:
7888
*
7989
* class ComponentContextParser {
80-
* public ComponentContext parseBytesToComponentContext(...) {
90+
* public ComponentIdentifierObj readComponentIdentifier(...) {
8191
* ...
82-
* if (ReadComponentIdentifierFingerprint() == null); // Inserted by this patch.
83-
* return emptyComponent;
92+
* if (extensionClass.filter(identifier, pathBuilder)) { // Inserted by this patch.
93+
* this.patch_isFiltered = true;
94+
* }
8495
* ...
8596
* }
8697
*
87-
* public ComponentIdentifierObj readComponentIdentifier(...) {
88-
* ...
89-
* if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch.
90-
* return null;
98+
* public Component parseBytesToComponentContext(...) {
9199
* ...
100+
* if (this.patch_isFiltered) { // Inserted by this patch.
101+
* return emptyComponent;
102+
* }
103+
* return originalUnpatchedComponent;
92104
* }
93105
* }
94106
*/
@@ -115,14 +127,13 @@ val lithoFilterPatch = bytecodePatch(
115127

116128
protobufBufferReferenceFingerprint.method.addInstruction(
117129
0,
118-
" invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
130+
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
119131
)
120132

121133
// endregion
122134

123135
// region Hook the method that parses bytes into a ComponentContext.
124136

125-
val readComponentMethod = readComponentIdentifierFingerprint.originalMethod
126137
// Get the only static method in the class.
127138
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.first { method ->
128139
AccessFlags.STATIC.isSet(method.accessFlags)
@@ -132,44 +143,47 @@ val lithoFilterPatch = bytecodePatch(
132143
builderMethodDescriptor.returnType == classDef.type
133144
}!!.immutableClass.fields.single()
134145

146+
// Add a field to store the result of the filtering. This allows checking the field
147+
// just before returning so the original code always runs the same when filtering occurs.
148+
val lithoFilterResultField = ImmutableField(
149+
componentContextParserFingerprint.classDef.type,
150+
"patch_isFiltered",
151+
"Z",
152+
AccessFlags.PRIVATE.value,
153+
null,
154+
null,
155+
null,
156+
).toMutable()
157+
componentContextParserFingerprint.classDef.fields.add(lithoFilterResultField)
158+
135159
// Returns an empty component instead of the original component.
136-
fun createReturnEmptyComponentInstructions(register: Int): String =
137-
"""
138-
move-object/from16 v$register, p1
139-
invoke-static { v$register }, $builderMethodDescriptor
140-
move-result-object v$register
141-
iget-object v$register, v$register, $emptyComponentField
142-
return-object v$register
143-
"""
160+
fun returnEmptyComponentInstructions(free: Int): String = """
161+
move-object/from16 v$free, p0
162+
iget-boolean v$free, v$free, $lithoFilterResultField
163+
if-eqz v$free, :unfiltered
164+
165+
move-object/from16 v$free, p1
166+
invoke-static { v$free }, $builderMethodDescriptor
167+
move-result-object v$free
168+
iget-object v$free, v$free, $emptyComponentField
169+
return-object v$free
170+
171+
:unfiltered
172+
nop
173+
"""
144174

145175
componentContextParserFingerprint.method.apply {
146176
// 19.18 and later require patching 2 methods instead of one.
147177
// Otherwise the modifications done here are the same for all targets.
148178
if (is_19_18_or_greater) {
149-
// Get the method name of the ReadComponentIdentifierFingerprint call.
150-
val readComponentMethodCallIndex = indexOfFirstInstructionOrThrow {
151-
val reference = getReference<MethodReference>()
152-
reference?.definingClass == readComponentMethod.definingClass &&
153-
reference.name == readComponentMethod.name
154-
}
155-
156-
// Result of read component, and also a free register.
157-
val register = getInstruction<OneRegisterInstruction>(readComponentMethodCallIndex + 1).registerA
179+
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
180+
val free = findFreeRegister(index)
158181

159-
// Insert after 'move-result-object'
160-
val insertHookIndex = readComponentMethodCallIndex + 2
161-
162-
// Return an EmptyComponent instead of the original component if the filterState method returns true.
163-
addInstructionsWithLabels(
164-
insertHookIndex,
165-
"""
166-
if-nez v$register, :unfiltered
167-
168-
# Component was filtered in ReadComponentIdentifierFingerprint hook
169-
${createReturnEmptyComponentInstructions(register)}
170-
""",
171-
ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
172-
)
182+
addInstructionsAtControlFlowLabel(
183+
index,
184+
returnEmptyComponentInstructions(free)
185+
)
186+
}
173187
}
174188
}
175189

@@ -178,47 +192,79 @@ val lithoFilterPatch = bytecodePatch(
178192
// region Read component then store the result.
179193

180194
readComponentIdentifierFingerprint.method.apply {
181-
val insertHookIndex = indexOfFirstInstructionOrThrow {
182-
opcode == Opcode.IPUT_OBJECT &&
183-
getReference<FieldReference>()?.type == "Ljava/lang/StringBuilder;"
195+
val returnIndex = indexOfFirstInstructionReversedOrThrow(Opcode.RETURN_OBJECT)
196+
if (indexOfFirstInstructionReversed(returnIndex - 1, Opcode.RETURN_OBJECT) >= 0) {
197+
throw PatchException("Found multiple return indexes") // Patch needs an update.
198+
}
199+
200+
val elementConfigClass = elementConfigFingerprint.originalClassDef
201+
val elementConfigClassType = elementConfigClass.type
202+
val elementConfigIndex = indexOfFirstInstructionReversedOrThrow(returnIndex) {
203+
val reference = getReference<MethodReference>()
204+
reference?.definingClass == elementConfigClassType
205+
}
206+
val elementConfigStringBuilderField = elementConfigClass.fields.single { field ->
207+
field.type == "Ljava/lang/StringBuilder;"
184208
}
185-
val stringBuilderRegister = getInstruction<TwoRegisterInstruction>(insertHookIndex).registerA
186209

187210
// Identifier is saved to a field just before the string builder.
188-
val identifierRegister = getInstruction<TwoRegisterInstruction>(
189-
indexOfFirstInstructionReversedOrThrow(insertHookIndex) {
211+
val putStringBuilderIndex = indexOfFirstInstructionOrThrow {
212+
val reference = getReference<FieldReference>()
213+
opcode == Opcode.IPUT_OBJECT &&
214+
reference?.definingClass == elementConfigClassType &&
215+
reference.type == "Ljava/lang/StringBuilder;"
216+
}
217+
val elementConfigIdentifierField = getInstruction<ReferenceInstruction>(
218+
indexOfFirstInstructionReversedOrThrow(putStringBuilderIndex) {
219+
val reference = getReference<FieldReference>()
190220
opcode == Opcode.IPUT_OBJECT &&
191-
getReference<FieldReference>()?.type == "Ljava/lang/String;"
192-
},
193-
).registerA
221+
reference?.definingClass == elementConfigClassType &&
222+
reference.type == "Ljava/lang/String;"
223+
}
224+
).getReference<FieldReference>()
225+
226+
// Could use some of these free registers multiple times, but this is inserting at a
227+
// return instruction so there is always multiple 4-bit registers available.
228+
val elementConfigRegister = getInstruction<FiveRegisterInstruction>(elementConfigIndex).registerC
229+
val identifierRegister = findFreeRegister(returnIndex, elementConfigRegister)
230+
val stringBuilderRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister)
231+
val thisRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister, stringBuilderRegister)
232+
val freeRegister = findFreeRegister(returnIndex, elementConfigRegister, identifierRegister, stringBuilderRegister, thisRegister)
194233

195-
val freeRegister = findFreeRegister(insertHookIndex, identifierRegister, stringBuilderRegister)
196234
val invokeFilterInstructions = """
235+
iget-object v$identifierRegister, v$elementConfigRegister, $elementConfigIdentifierField
236+
iget-object v$stringBuilderRegister, v$elementConfigRegister, $elementConfigStringBuilderField
197237
invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
198238
move-result v$freeRegister
199-
if-eqz v$freeRegister, :unfiltered
239+
move-object/from16 v$thisRegister, p0
240+
iput-boolean v$freeRegister, v$thisRegister, $lithoFilterResultField
200241
"""
201242

202-
addInstructionsWithLabels(
203-
insertHookIndex,
204-
if (is_19_18_or_greater) {
243+
if (is_19_18_or_greater) {
244+
addInstructionsAtControlFlowLabel(
245+
returnIndex,
246+
invokeFilterInstructions
247+
)
248+
} else {
249+
val elementConfigMethod = conversionContextFingerprint.originalClassDef.methods
250+
.single { method ->
251+
!AccessFlags.STATIC.isSet(method.accessFlags) && method.returnType == elementConfigClassType
252+
}
253+
254+
addInstructionsAtControlFlowLabel(
255+
returnIndex,
205256
"""
206-
$invokeFilterInstructions
257+
# Element config is a method on a parameter.
258+
move-object/from16 v$elementConfigRegister, p2
259+
invoke-virtual { v$elementConfigRegister }, $elementConfigMethod
260+
move-result-object v$elementConfigRegister
207261
208-
# Return null, and the ComponentContextParserFingerprint hook
209-
# handles returning an empty component.
210-
const/4 v$freeRegister, 0x0
211-
return-object v$freeRegister
212-
"""
213-
} else {
214-
"""
215262
$invokeFilterInstructions
216-
217-
${createReturnEmptyComponentInstructions(freeRegister)}
263+
264+
${returnEmptyComponentInstructions(freeRegister)}
218265
"""
219-
},
220-
ExternalLabel("unfiltered", getInstruction(insertHookIndex)),
221-
)
266+
)
267+
}
222268
}
223269

224270
// endregion

patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import app.revanced.patcher.patch.BytecodePatchContext
1111
import app.revanced.patcher.patch.PatchException
1212
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
1313
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
14+
import app.revanced.patcher.util.smali.ExternalLabel
1415
import app.revanced.patches.shared.misc.mapping.get
1516
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
1617
import app.revanced.patches.shared.misc.mapping.resourceMappings
@@ -207,6 +208,26 @@ fun MutableMethod.injectHideViewCall(
207208
"invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V",
208209
)
209210

211+
212+
/**
213+
* Inserts instructions at a given index, using the existing control flow label at that index.
214+
* Inserted instructions can have it's own control flow labels as well.
215+
*
216+
* Effectively this changes the code from:
217+
* :label
218+
* (original code)
219+
*
220+
* Into:
221+
* :label
222+
* (patch code)
223+
* (original code)
224+
*/
225+
// TODO: delete this on next major version bump.
226+
fun MutableMethod.addInstructionsAtControlFlowLabel(
227+
insertIndex: Int,
228+
instructions: String
229+
) = addInstructionsAtControlFlowLabel(insertIndex, instructions, *arrayOf<ExternalLabel>())
230+
210231
/**
211232
* Inserts instructions at a given index, using the existing control flow label at that index.
212233
* Inserted instructions can have it's own control flow labels as well.
@@ -223,13 +244,14 @@ fun MutableMethod.injectHideViewCall(
223244
fun MutableMethod.addInstructionsAtControlFlowLabel(
224245
insertIndex: Int,
225246
instructions: String,
247+
vararg externalLabels: ExternalLabel
226248
) {
227249
// Duplicate original instruction and add to +1 index.
228250
addInstruction(insertIndex + 1, getInstruction(insertIndex))
229251

230252
// Add patch code at same index as duplicated instruction,
231253
// so it uses the original instruction control flow label.
232-
addInstructionsWithLabels(insertIndex + 1, instructions)
254+
addInstructionsWithLabels(insertIndex + 1, instructions, *externalLabels)
233255

234256
// Remove original non duplicated instruction.
235257
removeInstruction(insertIndex)

0 commit comments

Comments
 (0)