Skip to content

Commit 8cc8a91

Browse files
author
LisoUseInAIKyrios
authored
refactor(YouTube - Litho): Use a simpler hook that does not require using a thread local (#5281)
1 parent 7f91038 commit 8cc8a91

File tree

3 files changed

+86
-139
lines changed

3 files changed

+86
-139
lines changed

extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public String toString() {
4848
/**
4949
* Search through a byte array for all ASCII strings.
5050
*/
51-
private static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
51+
static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
5252
// Valid ASCII values (ignore control characters).
5353
final int minimumAscii = 32; // 32 = space character
5454
final int maximumAscii = 126; // 127 = delete character
@@ -96,7 +96,7 @@ private static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
9696
private static final class DummyFilter extends Filter { }
9797

9898
private static final Filter[] filters = new Filter[] {
99-
new DummyFilter() // Replaced by patch.
99+
new DummyFilter() // Replaced patching, do not touch.
100100
};
101101

102102
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
@@ -108,11 +108,7 @@ private static final class DummyFilter extends Filter { }
108108
* Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
109109
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
110110
*/
111-
private static final ThreadLocal<ByteBuffer> bufferThreadLocal = new ThreadLocal<>();
112-
/**
113-
* Results of calling {@link #filter(String, StringBuilder)}.
114-
*/
115-
private static final ThreadLocal<Boolean> filterResult = new ThreadLocal<>();
111+
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
116112

117113
static {
118114
for (Filter filter : filters) {
@@ -168,57 +164,50 @@ private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
168164
/**
169165
* Injection point. Called off the main thread.
170166
*/
171-
@SuppressWarnings("unused")
172-
public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) {
167+
public static void setProtoBuffer(byte[] buffer) {
173168
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
174169
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
175170
// The buffer will be cleared from memory after a new buffer is set by the same thread,
176171
// or when the calling thread eventually dies.
177-
if (protobufBuffer == null) {
172+
bufferThreadLocal.set(buffer);
173+
}
174+
175+
/**
176+
* Injection point. Called off the main thread.
177+
* Targets 20.21 and lower.
178+
*/
179+
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
180+
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
181+
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
182+
// The buffer will be cleared from memory after a new buffer is set by the same thread,
183+
// or when the calling thread eventually dies.
184+
if (buffer == null || !buffer.hasArray()) {
178185
// It appears the buffer can be cleared out just before the call to #filter()
179186
// Ignore this null value and retain the last buffer that was set.
180-
Logger.printDebug(() -> "Ignoring null protobuffer");
187+
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
181188
} else {
182-
bufferThreadLocal.set(protobufBuffer);
189+
setProtoBuffer(buffer.array());
183190
}
184191
}
185192

186193
/**
187194
* Injection point.
188195
*/
189-
public static boolean shouldFilter() {
190-
Boolean shouldFilter = filterResult.get();
191-
return shouldFilter != null && shouldFilter;
192-
}
193-
194-
/**
195-
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
196-
*/
197-
public static void filter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
198-
filterResult.set(handleFiltering(lithoIdentifier, pathBuilder));
199-
}
200-
201-
private static boolean handleFiltering(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
196+
public static boolean shouldFilter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
202197
try {
203198
if (pathBuilder.length() == 0) {
204199
return false;
205200
}
206201

207-
ByteBuffer protobufBuffer = bufferThreadLocal.get();
208-
final byte[] bufferArray;
202+
byte[] buffer = bufferThreadLocal.get();
209203
// Potentially the buffer may have been null or never set up until now.
210204
// Use an empty buffer so the litho id/path filters still work correctly.
211-
if (protobufBuffer == null) {
212-
bufferArray = EMPTY_BYTE_ARRAY;
213-
} else if (!protobufBuffer.hasArray()) {
214-
Logger.printDebug(() -> "Proto buffer does not have an array, using an empty buffer array");
215-
bufferArray = EMPTY_BYTE_ARRAY;
216-
} else {
217-
bufferArray = protobufBuffer.array();
205+
if (buffer == null) {
206+
buffer = EMPTY_BYTE_ARRAY;
218207
}
219208

220-
LithoFilterParameters parameter = new LithoFilterParameters(lithoIdentifier,
221-
pathBuilder.toString(), bufferArray);
209+
LithoFilterParameters parameter = new LithoFilterParameters(
210+
lithoIdentifier, pathBuilder.toString(), buffer);
222211
Logger.printDebug(() -> "Searching " + parameter);
223212

224213
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,18 @@ import com.android.tools.smali.dexlib2.AccessFlags
77
import com.android.tools.smali.dexlib2.Opcode
88

99
internal val componentContextParserFingerprint = fingerprint {
10-
strings(
11-
"TreeNode result must be set.",
12-
// String is a partial match and changed slightly in 20.03+
13-
"it was removed due to duplicate converter bindings."
14-
)
10+
strings("Number of bits must be positive")
1511
}
1612

17-
/**
18-
* Resolves to the class found in [componentContextParserFingerprint].
19-
* When patching 19.16 this fingerprint matches the same method as [componentContextParserFingerprint].
20-
*/
21-
internal val componentContextSubParserFingerprint = fingerprint {
13+
internal val componentCreateFingerprint = fingerprint {
2214
strings(
23-
"Number of bits must be positive"
15+
"Element missing correct type extension",
16+
"Element missing type"
2417
)
2518
}
2619

2720
internal val lithoFilterFingerprint = fingerprint {
2821
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
29-
returns("V")
3022
custom { _, classDef ->
3123
classDef.endsWith("/LithoFilterPatch;")
3224
}
@@ -58,7 +50,7 @@ internal val lithoThreadExecutorFingerprint = fingerprint {
5850
parameters("I", "I", "I")
5951
custom { method, classDef ->
6052
classDef.superclass == "Ljava/util/concurrent/ThreadPoolExecutor;" &&
61-
method.containsLiteralInstruction(1L) // 1L = default thread timeout.
53+
method.containsLiteralInstruction(1L) // 1L = default thread timeout.
6254
}
6355
}
6456

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

Lines changed: 56 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
99
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
1010
import app.revanced.patcher.patch.bytecodePatch
1111
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
12+
import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater
1213
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
1314
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
1415
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
1516
import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
1617
import app.revanced.util.addInstructionsAtControlFlowLabel
1718
import app.revanced.util.findFreeRegister
18-
import app.revanced.util.findInstructionIndicesReversedOrThrow
1919
import app.revanced.util.getReference
2020
import app.revanced.util.indexOfFirstInstructionOrThrow
2121
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
@@ -24,7 +24,6 @@ import com.android.tools.smali.dexlib2.Opcode
2424
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
2525
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
2626
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
27-
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
2827

2928
lateinit var addLithoFilter: (String) -> Unit
3029
private set
@@ -66,17 +65,11 @@ val lithoFilterPatch = bytecodePatch(
6665
* }
6766
* }
6867
*
69-
* class ComponentContextParser {
70-
* public Component parseComponent() {
68+
* class CreateComponentClass {
69+
* public Component createComponent() {
7170
* ...
7271
*
73-
* // Checks if the component should be filtered.
74-
* // Sets a thread local with the filtering result.
75-
* extensionClass.filter(identifier, pathBuilder); // Inserted by this patch.
76-
*
77-
* ...
78-
*
79-
* if (extensionClass.shouldFilter()) { // Inserted by this patch.
72+
* if (extensionClass.shouldFilter(identifier, path)) { // Inserted by this patch.
8073
* return emptyComponent;
8174
* }
8275
* return originalUnpatchedComponent; // Original code.
@@ -116,95 +109,68 @@ val lithoFilterPatch = bytecodePatch(
116109
// Allow the method to run to completion, and override the
117110
// return value with an empty component if it should be filtered.
118111
// It is important to allow the original code to always run to completion,
119-
// otherwise memory leaks and poor app performance can occur.
120-
//
121-
// The extension filtering result needs to be saved off somewhere, but cannot
122-
// save to a class field since the target class is called by multiple threads.
123-
// It would be great if there was a way to change the register count of the
124-
// method implementation and save the result to a high register to later use
125-
// in the method, but there is no simple way to do that.
126-
// Instead save the extension filter result to a thread local and check the
127-
// filtering result at each method return index.
128-
// String field for the litho identifier.
129-
componentContextParserFingerprint.method.apply {
130-
val conversionContextClass = conversionContextFingerprintToString.originalClassDef
131-
132-
val conversionContextIdentifierField = componentContextSubParserFingerprint.match(
133-
componentContextParserFingerprint.originalClassDef
134-
).let {
135-
// Identifier field is loaded just before the string declaration.
136-
val index = it.method.indexOfFirstInstructionReversedOrThrow(
137-
it.stringMatches!!.first().index
138-
) {
139-
val reference = getReference<FieldReference>()
140-
reference?.definingClass == conversionContextClass.type
141-
&& reference.type == "Ljava/lang/String;"
142-
}
143-
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()
112+
// otherwise high memory usage and poor app performance can occur.
113+
114+
// Find the identifier/path fields of the conversion context.
115+
val conversionContextIdentifierField = componentContextParserFingerprint.let {
116+
// Identifier field is loaded just before the string declaration.
117+
val index = it.method.indexOfFirstInstructionReversedOrThrow(
118+
it.stringMatches!!.first().index
119+
) {
120+
val reference = getReference<FieldReference>()
121+
reference?.definingClass == conversionContextFingerprintToString.originalClassDef.type
122+
&& reference.type == "Ljava/lang/String;"
144123
}
145124

146-
// StringBuilder field for the litho path.
147-
val conversionContextPathBuilderField = conversionContextClass.fields
148-
.single { field -> field.type == "Ljava/lang/StringBuilder;" }
125+
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()!!
126+
}
149127

150-
val conversionContextResultIndex = indexOfFirstInstructionOrThrow {
151-
val reference = getReference<MethodReference>()
152-
reference?.returnType == conversionContextClass.type
153-
} + 1
128+
val conversionContextPathBuilderField = conversionContextFingerprintToString.originalClassDef
129+
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
154130

155-
val conversionContextResultRegister = getInstruction<OneRegisterInstruction>(
156-
conversionContextResultIndex
157-
).registerA
131+
// Find class and methods to create an empty component.
132+
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
133+
// The only static method in the class.
134+
method -> AccessFlags.STATIC.isSet(method.accessFlags)
135+
}
136+
val emptyComponentField = classBy {
137+
// Only one field that matches.
138+
it.type == builderMethodDescriptor.returnType
139+
}!!.immutableClass.fields.single()
140+
141+
componentCreateFingerprint.method.apply {
142+
val insertIndex = if (is_19_17_or_greater) {
143+
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
144+
} else {
145+
// 19.16 clobbers p2 so must check at start of the method and not at the return index.
146+
0
147+
}
158148

159-
val identifierRegister = findFreeRegister(
160-
conversionContextResultIndex, conversionContextResultRegister
161-
)
162-
val stringBuilderRegister = findFreeRegister(
163-
conversionContextResultIndex, conversionContextResultRegister, identifierRegister
164-
)
149+
val freeRegister = findFreeRegister(insertIndex)
150+
val identifierRegister = findFreeRegister(insertIndex, freeRegister)
151+
val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister)
165152

166-
// Check if the component should be filtered, and save the result to a thread local.
167153
addInstructionsAtControlFlowLabel(
168-
conversionContextResultIndex + 1,
154+
insertIndex,
169155
"""
170-
iget-object v$identifierRegister, v$conversionContextResultRegister, $conversionContextIdentifierField
171-
iget-object v$stringBuilderRegister, v$conversionContextResultRegister, $conversionContextPathBuilderField
172-
invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)V
156+
move-object/from16 v$freeRegister, p2
157+
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
158+
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
159+
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
160+
move-result v$freeRegister
161+
if-eqz v$freeRegister, :unfiltered
162+
163+
# Return an empty component
164+
move-object/from16 v$freeRegister, p1
165+
invoke-static { v$freeRegister }, $builderMethodDescriptor
166+
move-result-object v$freeRegister
167+
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
168+
return-object v$freeRegister
169+
170+
:unfiltered
171+
nop
173172
"""
174173
)
175-
176-
// Get the only static method in the class.
177-
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
178-
method -> AccessFlags.STATIC.isSet(method.accessFlags)
179-
}
180-
// Only one field.
181-
val emptyComponentField = classBy { classDef ->
182-
classDef.type == builderMethodDescriptor.returnType
183-
}!!.immutableClass.fields.single()
184-
185-
// Check at each return value if the component is filtered,
186-
// and return an empty component if filtering is needed.
187-
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { returnIndex ->
188-
val freeRegister = findFreeRegister(returnIndex)
189-
190-
addInstructionsAtControlFlowLabel(
191-
returnIndex,
192-
"""
193-
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter()Z
194-
move-result v$freeRegister
195-
if-eqz v$freeRegister, :unfiltered
196-
197-
move-object/from16 v$freeRegister, p1
198-
invoke-static { v$freeRegister }, $builderMethodDescriptor
199-
move-result-object v$freeRegister
200-
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
201-
return-object v$freeRegister
202-
203-
:unfiltered
204-
nop
205-
"""
206-
)
207-
}
208174
}
209175

210176
// endregion

0 commit comments

Comments
 (0)