安卓学习
这是第二篇
我们继续
四大组件的理解
用AndroidKiller.exe
打开土豆的apk
1.Activity(活动):用于表现功能
2.Service(服务):后台运行服务,不提供界面呈现
3.Broadcast receiver(广播接收者):用于接收广播
4.Content provider(内容提供者):支持多个应用中存储和读取数据,相当于数据库
一、Activity(活动)
创建
1、声明
Activity是一个界面,一个APP是由很多Activity进行界面调用的
想要使用Activity需要在AndroidManifest.xml
中声明,只要调用的就需要声明
2、继承Activity
然后接着上一节课 使用eclipse
进行打开
创建安卓项目
项目名
这里是选初始图标
有没有很熟悉 就是安卓学习(一)中的改图标实操
一路默认过去
创建完成之后 主函数 在这里
这里有一个要注意的点:
无论是做什么,都需要在应用程序的AndroidManifest.xml
必须要声明所有的组建
public class MainActivity extends Activity
继承Activity才能被识别为一个活动界面MainActivity
进行设计我们的界面
这里有很多 可以自己玩一玩
将我们设计的进行导出
我们本地的模拟器 会出现在这里进行安装
进行导入别人的项目包
进行选择目录
运行过程
1、onCreat()
这个方法在活动第一次被创建的时候调用。在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等
2.onStart()
这个方法在活动由不可见变为可见的时侯进行调用!!界面呈现出来了!
3.`onResume()
这个方法在活动准备好和用户进行交互的时侯进行调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。
运行的结果 就是展示内容
销毁过程
1、onPause()
这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度定要快,不然会影响到新的栈顶活动的使用。
白化:手机向上一滑回到桌面 应用进行最小化 放在后台运行 那么 在向上一滑可以选择执行应用
2、onStop()
这个方法在活动完全不可见的时侯调用。它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而nonStop()方法并不会执行。
3、onDestroy()
这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。
白化:手机向上一滑回到桌面 应用进行最小化 放在后台运行 那么 在向上一滑可以结束应用
4、onRestart()
这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
白化:应用关闭 进行重新启动
二、Service(服务)
一般概念
Android中的服务,它与Activity不同,它是不能与用户交互的,不能自己启动的,运行在后台的程序如果我们退出应用时,Service进程并没有结束,它仍然在后台运行
1、比如我们播放音乐的时候,有可能想边听音乐边干些其他事情,当我们退出播放音乐的应用,如果不用Service,我们就听不到歌了,所以这时便就得用到Service了
2、比如当我们一个应用的数据是通过网络获取的,不同时间(一段时间)的数据是不同的这时候我们可以用Service在后台定时更新,而不用每打开应用的时候在去获取。就像微信里面的语音一样!
Service生命周期
前言
Service的生命周期并不像Activity那么复杂,它只继承的是onCreate()
、onStart()
、onDestroy()
三个方法
当我们第一次启动Service时,先后调用onCreate()
和onStart()
这两个方法,当停止Service时,则执行onDestroy()
方法
这里注意:如果Service已经启动了,当我们再次启动Service时,不会在执行onCreate()方法,而是直接执行onStart()方法。
startservice启动Service的生命周期
执行startservice时,Service会经历onCreate -> onStartcommand
(函数API)。
执行stopService时,Service直接调用onDestroy方法。
调用者如果没有stopService,Service会一直在后台运行,下次调用者再起来仍然可以stopService。
如果不去触发,就一直在后台运行
bondservice启动Service的生命周期
执行bondservice时,Service会经历onCreate -> onBind
这个时候调用者和Service绑定在一起。
调用者调用unbindService
方法或者调用者Context不存在了(如Activity被 finish了),Service就会调用onUnbind-> onDestroy
这里所谓的绑定在一起就是说两者是共同存在和消失的
图解
三、Broadcast receiver(广播接收者)
1、前言
Broadcast receiver:广播接收者,就是接收来自android内部的广播,对接收到的广播进行选择处理,想要接收什么样的广播和内部定义的广播匹配,匹配则进行该做的处理操作,没有匹配则无操作
比如在刷抖音的同时接收到短信事件,对此你要做什么操作,是想看短信内容还是不做什么处理继续刷抖音,这就是广播的用途, android内部的四大内置类之一:Broadcast receiver
广播接收者的四大用途
1、首先我们要有一个发送广播的按钮,对收到的广播进行处理
2、编写广播接收类Receiver,继承BroadcastReceiver
public class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
System.out.println("收到了");
}
}
注:onReceive()方法是对接收匹配的广播的处理操作
3、因BroadcastReceiver是android内置类就如Activity类一样需要在AndroidManifest.xml文件进行声明
<receiver android:name=".Receiver">
<intent-filter > <action android:name="android.intent.action.name"/>
</intent-filter>
</receiver>
其中action的name属性是自己定义的名字,想要匹配广播则发送的广播和此名称一致即可对其处理
4、main.xml代码
点击按钮设置intent的action需要和上面写的一致:android.intent.action.name
则接收者就可接收到广播对其操作
注:sendBroadcast()是发送广播
public class MainActivity extends Activity {
private Button send;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
send=(Button) this.findViewById(R.id.send);
send.setOnClickListener(new OnClickListener() {
public void onClick(View v) { Intent i=new Intent();
i.setAction("android.intent.action.name");
sendBroadcast(i);
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
那么结果 自然就是输出文本:收到了
2、Notification(状态栏通知)
0x01、理解
首先,发送一个状态栏通知必须用到两个类:NotificationManager
、Notification
NotificationManager:是状态栏通知的管理类,负责发通知、清除通知等。NotificationManager是一个系统 Service,必须通过 getSystemService()方法来获取。
Notifcation:是具体的状态栏通知对象,可以设置icon、文字、提示声音、振动等参数。
0x02、使用
1、创建 Notification
通过 NotificationManager的notify(int、Notification)方法来启动 Notifcation
第一个参数唯一的标识该 Notifcation,第二个参数就是 Notifcation对象。
2、更新 Notification
调用 Notification的 setLatestEventInfo
方法来更新内容,然后再调用 NotifcationManager的 notify()方法
3、删除 Notification
通过 NotifcationManager的cancel(int)
方法,来清除某个通知。其中参数就是 Notifcation的唯一标识D
当然也可以通过cancelAll
(来清除状态栏所有的通知)
4、Notification设置(振动、铃声等)
0x03、状态栏通知(Notification)参数的设置
//新建状态栏通知
baseNF = new Notification();
//设置通知在状态栏显示的图标
baseNF.icon = R.drawable.icon;
//通知时在状态栏显示的内容
baseNF.tickerText = "You clicked BaseNF!";
//通知的默认参数
DEFAULT_SOUND, DEFAULT_VIBRATE, DEFAULT_LIGHTS
//如果要全部采用默认值,用DEFAULT_ALL
//采用默认声音
baseNF.defaults = Notification.DEFAULT_SOUND;
//第二个参数:下拉状态栏时显示的消息标题 expanded message title
//第三个参数:下拉状态栏时显示的消息内容 expanded message text
//第四个参数:点击该通知时执行页面跳转
baseNF.setLatestEventInfo(Lesson_10.this, "Title01", "Content01", pd);
//发出状态栏通知
nm.notify(Notification_ID_BASE, baseNF);
0x04、PendingIntent
前言
与Intent有些类似,它们之间也确实存在着不少共同点
相同:都可以去指明某一个“意图”,都可以用于启动活动、启动服务以及发送广播等。
不同:Intent更加倾向于去立即执行某个动作,而 PendingIntent更加倾向于在某个合适的时机去执行某个动作。
所以,也可以把PendingIntent 简单地理解为延迟执行的Intent。
获取PendingIntent的实例
通过三个静态的方法︰
getActivity()方法
getBroadcast()方法
getService()方法
3、深入去理解
0x01、广播接收者(Broadcast receiver)的理解
我们的应用可以使用它对外部事件进行过滤只对感兴趣的外部事件(如当电话接入时,或者数据网络可用时)
进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个 activity或 service来响应它们收到的信息,或者用 NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。
0x02、广播类型
1、普通广播,通过Context.sendBroadcast(Intentmylntent)
发送的。
2、有序广播,通过Context.sendOrderedBroadcast(intent,receiverPermission)
发送的
该方法第2个参数决定该广播的级别,级别数值是在-1000~1000
之间,值越大,发送的优先级越高;广播接收者接收广播时的级别级别,可通过intentfilter
中的priority
进行设置设为2147483647时优先级最高,同级別接收的先后是随机的,再到级别低的收到广播,高级别的或同级别先接收到广播的可以通过aboutBrodcast()
方法截断广播使其他的接收者无法收到该广播,还有其他构造函数。
3、异步广播,
通过Context.sendStickyBroadcast(Intent myIntent)
发送的,还有sendStickyOrderedBroadcast(intent,resultReceiver,scheduler,initialCode,initialData,initialExtras)
方法,该方法具有有序广播的特性也有异步广播的特性;
发送异步广播要:权限,接收并处理完 Intent后,广播依然存在,直到你调用 removeStickyBroadcast(intent)主动把它去掉
注意:发送广播时的intent参数与Contex.startActivity
启动起来的 Intent不同前者可以被多个订阅它的广播接收器调用,后者只能被一个(Activity或 service)
调用。
3、监听广播 Intent步骤:
写一个继承 BroadCastReceiver的类重写onReceive()
方法,广播接收器仅在它执行这个方法时处于活跃状态。
当 onReceive()返回后,它即为失活状态,注意为了保证用户交互过程的流畅,一些费时的操作要放到线程里
如类名SMSBroadcastReceiver
注册该广播接收者,注册有两种方法程序动态注册和Androidmanifest文件中进行静态注册
1、静态注册的广播,下面的 priority表示接收广播的级别"2147483647"为最高优先级
2、动态注册,一般在 Activity可交互时onResume()
内注册BroadcastReceiver
IntentFilter intentFilter=new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
registerReceiver(mBatteryInfoReceiver ,intentFilter);
//取消注册
unregisterReceiver(receiver);
<!-- 检测网络的权限 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
4、注意事项
1、生命周期只有十秒左右
如果在onreceive()
内做超过十秒内的事情,就会报ANR(Application no Response)
程序无响应的错误信息
如果需要完成一项比较耗时的工作,应该通过发送Intent给Service,由 Service来完成。
这里不能使用子线程来解决,因为 BroadcastReceiver的生命周期很短,子线程可能还没有结束 BroadcastReceiver就先结束了.BroadcastReceiver
一旦结束,此时BroadcastReceiver的所在进程很容易在系统需要内存时被优先杀死,因为它属于空进程(没有任何活动组件的进程)。如果它的宿主进程被杀死,那么正在工作的子线程也会被杀死。所以采用子线程来解决是不可靠的。
2、动态注册广播接收器的其他特点
当用来注册的 Activity关掉后,广播也就失效了。静态注册无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器也是打开着的。也就是说哪怕APP本身未启动,该APP订阅的广播在触发时也会对它起作用系统常见广播 Intent
如开机启动、电池电量变化、时间的变化等广播。
总结
手机每天很多推送,推送就是广播接收者,怎么将信息推送岀来,就是以上阐述内容
四、内容提供者(Content provider)
1、Content Providers的概念
内容提供器(Content provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是 Android实现跨程序共享数据的标准方式。不同于文件存储和 SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
2、Content Providers的两种方法
1)使用现有的内容提供器来读取和操作相应程序中的数据;
2)创建自己的内容提供器给我们程序的数据提供外部访问接口。
3、访问其他程序中的数据
当一个应用程序通过内容提供器对其数据提供了外部访问接口,任何其他的应用程序就都可以对这部分数据进行访问。Android系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口。
1、ContentResolver的使用
0x01、前言
如果想要访问內容提供器中共享的数据,就一定要写ContentResolver
类,可以通过Context中的getContentResolver方法获取到该类的实例。
0x02、ContentResolver
提供了一系列的方法用于对数据进行CRUD操作,其中 insert(方法用于添加数据,update方法用于更新数据,delete方法用于删除数据,query方法用于查询数据)。
但不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI.
0x03、UrI的理解
内容URI给內容提供器中的数据建立了唯一标识符,它主要由两部分组成,权限(authority)和路径(path)
。
权限是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。
路径则是用于对同一应用程序中不同的表做区分的,通常都会添加到权限的后面。
第一部分:content://相当于http://第二部分:com.eyy5.app.provider相当于vip.qiyikt.com第三部分:table1相当于forum
0x04、查询
uri:指定查询某个应用程序下的某一张表projection:指定查询的列名selection:指定 where的约束条件selectionArgs:为 where中的占位符提供具体的值sortOrder:指定查询结果的排序方式Cursor cursor = getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder);if (cursor != null) {while (cursor.moveToNext()) {String column1 = cursor.getString(cursor.getColumnIndex("column1"));int column2 = cursor.getInt(cursor.getColumnIndex("column2"));}cursor.close();}
0x05、添加
ContentValues values = new ContentValues();values.put("column1", "text");values.put("column2", 1);getContentResolver().insert(uri, values);
0x06、修改
ContentValues values = new ContentValues();values.put("column1", "");getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});
0x07、删除
getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });
2、使用 SharedPreferences存储数据
Shared Preferences的概念
保存基于XML文件存储的key-value键值对数据,通常用来存储一些简单的配置信息。通过DDMs的File Explorer面板,展开文件浏览树,很明显 SharedPreferences数据总是存储在/data/data/shared_prefs目录下。SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过SharedPreferences.edit()
获取的内部接口 Editor对象实现。
3、文件存储方式
文件存储数据的概念cookie
文件存储是 Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,这样方便于之后将数据从文件中重新解析出来。
文件存储数据的实现步骤
Context类中提供了一个openFileOutput()
方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称
注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/files/
目录下的
第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND
其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在就往文件里面追加内容,不存在就创建新文件。其实文件的操作模式本来还有另外两种,MODE_WORLD_READABLE和 MODE_WORLD_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞,现已在 Android4.2版本中被废弃
Context还提供了如下几个重要的方法
getDir(String name҅int mode):在应用程序的数据文件夹下获取或者创建name对应的子目录File getFilesDir():获取该应用程序的数据文件夹的绝对路径String[] fileList():返回该应用数据文件夹的全部文件
4、SQLite存储数据
1、SQLite数据库的概念
SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百K的內存就足够了
因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地持久化的功能有了一次质的飞跃。
2、如何创建数据库
Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这类就可以非常简单地对数据库进行创建和升级。
注意
SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个帮助类去继承SQLiteOpenHelper
SQLiteOpenHelper中有两个抽象方法,分别是onCreate()
和 onUpgrade()
我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
SQLiteOpenHelper的构造方法
//context:上下文对象//name:数据库名,创建数据库时使用的就是这里指定的名称//factory:允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入null/version:当前数据库的版本号,可用于对数据库进行升级操作
public MyDatabaseHelper(Context context, String name,CursorFactory factory, int version) { super(context, name, factory, version);}
5、网络数据存储
使用HTTP协议访问网络
工作原理:就是客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数
据进行解析和处理就可以了。
1)使用 HttpURLConnection
常用的方法主要有两个,GET和POST。GET表示希望从服务器那里获取数据,而POST则表示希望提交数据给服务器。
//GETconn.setRequestMethod("GET");//POSTconn.setRequestMethod("POST");//连接超时conn.setConnectTimeout(8000);//读取超时conn.setReadTimeout(8000);
HttpClient
HttpClient是Apache提供的HTTP网络访问接口,从一开始的时候就被引入到了AndroidAPI中。
HttpClient是一个接口,因此无法创建它的实例,通常情况下都会创建一个DefaultHttpClient的实例
HttpClient httpClient = new DefaultHttpClient();GET请求:HttpGet httpGet = new HttpGet("http://www.baidu.com");httpClient.execute(httpGet);POST请求:HttpPost httpPost = new HttpPost("http://www.baidu.com");List<NameValuePair> params = new ArrayList<NameValuePair>();params.add(new BasicNameValuePair("username", "admin"));params.add(new BasicNameValuePair("password", "123456"));UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "utf-8");httpPost.setEntity(entity);httpClient.execute(httpPost);
Dalvik字节码
前言
Dalvik就是smail代码一种编写形式,在Java代码里面不能去修改某个逻辑,那么把java代码编译成smail代码,就是把dex文件转换为smail文件,也就说Java和 smali进行了一个翻译,那么记住 Dalvik里面的smail是可以修改的,java代码是修改不了的
破解的原理:就是把Java改成smail,用smail去修改之后在回编译回去同时java逻辑也发生的改变
1、dalvik寄存器
32位,支持所有类型,<=32的一个寄存器
这个时候 我们要考虑一个问题
如果寄存器里面的东西超过32位怎么办?
使用32位两个相邻的寄存器就是64位,所以64位就是两个相邻的寄存器
2、寄存器的命名法
v命名法:局部变量寄存器v0-vn参数寄存器vn-vn+m
p命名法参数寄存器p0-pn变量寄存器v0-vn
smail代码 看不懂 可以转换看Java代码
简单分析一下
v命名
分析:
这个demo里面有一个 getHelloworld一个方法,括号里面传的是参数javalangstring就是参数
L
是JAVA中的java类型,对应Dalvik字节码类型就是L
I
是JAVA中的int类型,对应Dalvik字节码类型就是I
;
是隔开两个参数,返回值类型就是 string
regsize表示寄存器有5个
此时看到的是调用方法,v0~v4
第一个红框调用了一个方法把V2、V3存了进去,返回了一个v2,v2和v3是变量寄存器返回了v2,v2包含v3
第二个红框v0和v4做一个参数寄存器返回了v0,那么3是什么?v3在这里已经开始被v2返回掉了
注:invoke-virtual
是调用一个虚方法一个直接方法的意思
p命名
分析:
这个demo同样是 getHelloworld方法
第一个红框在java/lang/StringBuilder
类里面调用了一个 append的方法拼接传来string,然后返回了一个java/lang/StringBuilder类型,把p0传了进去,p0前面看是参数,这里就是把p0作为参数传了进去调用了v1,就是v1里面传入了个p0,然后在返回一个 javalangstring,最后返回了v1,v1本来是存在的但他传入了个p0,但是他还是v1,返回了move object
对象!所以他返回了个v0,所以这里v0作为参数寄存器
第二个红框中,p1这时候作为一个参数继续传给v0,继续最终返回v0,所以这样就可以理解了p0和p1为参数寄存器
3、dex文件反汇编工具
0x01、.java
编译成.class
在编译成.dex
,最后反编译得到samli文件
.java ! .class ! .dex ! smali
0x02、dx.jar
:.class
打包.dex
dx --dex --output=Decrypt.dex com/yijinda/demo/Decrypt.cl ass
使用dex将指定目录下的 class打包成dex
0x03、Baksmali.jar:.dex
反汇编成smali(反编译)
java -jar baksmali.jar -o smali_out/ classes.dex
0x04、Smali.jar:.smali
打包成.dex
(回编译)
java -jar smali.jar smali_out/ -o classes.dex
4、Dalvik字节码类型
注意:除了红色字体 其他java对应的Dalvik字节码都是首字母
5、字段
Lpackage/name/ObjectName; --->FieldName: Ljava/lang/String;
字段格式:类型(包名+类名)--->字段名称:字段类型
解释:一个完整的类里面有方法、变量,字段表示的是变量,是成员变量的意思
Package com.dayu.demoClass dome{String FieldName;}
6、方法
Lpackage/name/ObjectName;--->MethodName(III)Z
(III)Z:这部分表示的是方法的签名信息
Package com.dayu.demoClass dome{String FieldName;Public boolen MethodName(int1,int2,int3){};}
实操
介绍其他的一个小工具:jadx-gui-1.2.0.exe
它是绿色版的
很小的 直接点击使用 即可
当我们点击购买 取消之后
它会弹出:支付取消
把贪吃蛇的APK直接丢到jadx-gui
中
进行搜索
找到之后 进行分析
支付失败和支付取消 都调用了zombie.BuyFailed();
这个方法
进一步查看这个方法
ctrl+鼠标左键
public static native void BuyFailed(); #支付失败public static native void BuySccess(); #支付成功
那么这里的思路 就是把支付失败和支付取消的代码 都替换成支付成功的代码,然后进行反编译输出
这边我还是习惯使用AK:AndroidKiller
这里要注意:
要进行文本转Unicode
const-string v1, "\u652f\u4ed8\u6210\u529f"const/4 v2, 0x0invoke-static {v0, v1, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v0invoke-virtual {v0}, Landroid/widget/Toast;->show()V.line 121invoke-static {}, Lcom/qy/zombie/zombie;->BuySccess()V.line 122return-void
进行替换
然后保存 反编译输出
进行查看
破解成功
这里有很多 可以自己玩一玩
- 本文作者: 略略略
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/332
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!